from flask import ( Blueprint, jsonify, request, abort, current_app, render_template, redirect, url_for, ) from flask_login import login_required, current_user from app.core.models import Subnet, Server, App, Port, Location from app.core.extensions import db from app.scripts.ip_scanner import scan import random import ipaddress from flask_wtf import CSRFProtect import markdown from datetime import datetime from flask import flash from app.utils.app_utils import is_port_in_use, validate_port_data from difflib import SequenceMatcher import json bp = Blueprint("api", __name__, url_prefix="/api") csrf = CSRFProtect() @bp.route("/subnets", methods=["GET"]) @csrf.exempt @login_required def get_subnets(): """Get all subnets grouped by site""" try: subnets = Subnet.query.filter_by(user_id=current_user.id).all() # Group subnets by location (site) sites = {} for subnet in subnets: location = subnet.location_ref # Make sure this attribute matches your model relationship if not location: location_name = "Unassigned" location_id = None else: location_name = location.name location_id = location.id if location_id not in sites: sites[location_id] = { "name": location_name, "id": location_id, "subnets": [] } sites[location_id]["subnets"].append({ "id": subnet.id, "cidr": subnet.cidr, "location_id": location_id }) # Convert to list of site objects result = list(sites.values()) return jsonify(result) except Exception as e: print(f"Error loading subnets: {e}") # Add debugging return jsonify({"error": str(e)}), 500 @bp.route("/subnets/", methods=["GET"]) @login_required def get_subnet(subnet_id): """Get details for a specific subnet""" subnet = Subnet.query.get_or_404(subnet_id) servers = [] for server in Server.query.filter_by(subnet_id=subnet_id).all(): servers.append( { "id": server.id, "hostname": server.hostname, "ip_address": server.ip_address, "created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"), } ) result = { "id": subnet.id, "cidr": subnet.cidr, "location": subnet.location, "used_ips": subnet.used_ips, "auto_scan": subnet.auto_scan, "created_at": subnet.created_at.strftime("%Y-%m-%d %H:%M:%S"), "servers": servers, } return jsonify(result) @bp.route("/subnets//scan", methods=["POST"]) @login_required def api_subnet_scan(subnet_id): """Scan a subnet via API""" subnet = Subnet.query.get_or_404(subnet_id) try: results = scan(subnet.cidr, save_results=True) return jsonify( { "success": True, "subnet": subnet.cidr, "hosts_found": len(results), "results": results, } ) except Exception as e: return ( jsonify({"success": False, "message": f"Error scanning subnet: {str(e)}"}), 500, ) @bp.route("/servers", methods=["GET"]) @login_required def get_servers(): """Get all servers""" servers = Server.query.all() result = [] for server in servers: result.append( { "id": server.id, "hostname": server.hostname, "ip_address": server.ip_address, "subnet_id": server.subnet_id, "created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"), } ) return jsonify({"servers": result}) @bp.route("/servers/", methods=["GET"]) @login_required def get_server(server_id): """Get a specific server""" server = Server.query.get_or_404(server_id) apps = [] for app in server.apps: ports = [] for port in app.ports: ports.append( { "id": port.id, "port_number": port.port_number, "protocol": port.protocol, "description": port.description, } ) apps.append( { "id": app.id, "name": app.name, "ports": ports, "created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"), } ) result = { "id": server.id, "hostname": server.hostname, "ip_address": server.ip_address, "subnet_id": server.subnet_id, "documentation": server.documentation, "apps": apps, "created_at": server.created_at.strftime("%Y-%m-%d %H:%M:%S"), } return jsonify(result) @bp.route("/apps", methods=["GET"]) @login_required def get_apps(): """Get all applications""" apps = App.query.all() result = [] for app in apps: result.append( { "id": app.id, "name": app.name, "server_id": app.server_id, "created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"), } ) return jsonify({"apps": result}) @bp.route("/apps/", methods=["GET"]) @login_required def get_app(app_id): """Get details for a specific application""" app = App.query.get_or_404(app_id) result = { "id": app.id, "name": app.name, "server_id": app.server_id, "documentation": app.documentation, "created_at": app.created_at.strftime("%Y-%m-%d %H:%M:%S"), "ports": app.ports, } return jsonify(result) @bp.route("/status", methods=["GET"]) def status(): return jsonify({"status": "OK"}) @bp.route("/ports/suggest", methods=["GET"]) def suggest_ports(): app_type = request.args.get("type", "").lower() # Common port suggestions based on app type suggestions = { "web": [ {"port": 80, "type": "tcp", "desc": "HTTP"}, {"port": 443, "type": "tcp", "desc": "HTTPS"}, ], "database": [ {"port": 3306, "type": "tcp", "desc": "MySQL"}, {"port": 5432, "type": "tcp", "desc": "PostgreSQL"}, {"port": 1521, "type": "tcp", "desc": "Oracle"}, ], "mail": [ {"port": 25, "type": "tcp", "desc": "SMTP"}, {"port": 143, "type": "tcp", "desc": "IMAP"}, {"port": 110, "type": "tcp", "desc": "POP3"}, ], "file": [ {"port": 21, "type": "tcp", "desc": "FTP"}, {"port": 22, "type": "tcp", "desc": "SFTP/SSH"}, {"port": 445, "type": "tcp", "desc": "SMB"}, ], } if app_type in suggestions: return jsonify(suggestions[app_type]) # Default suggestions return jsonify( [ {"port": 80, "type": "tcp", "desc": "HTTP"}, {"port": 22, "type": "tcp", "desc": "SSH"}, ] ) @bp.route("/servers//suggest_port", methods=["GET"]) @login_required def suggest_port(server_id): """Suggest a random unused port for a server""" server = Server.query.get_or_404(server_id) # Get all used ports for this server used_ports = [] for app in server.apps: for port in app.ports: used_ports.append(port.port_number) # Find an unused port in the dynamic/private port range available_port = None attempts = 0 while attempts < 50: # Try 50 times to find a random port # Random port between 10000 and 65535 port = random.randint(10000, 65535) if port not in used_ports: available_port = port break attempts += 1 if available_port is None: # If no random port found, find first available in sequence for port in range(10000, 65536): if port not in used_ports: available_port = port break return jsonify({"port": available_port}) @bp.route("/app//add-port", methods=["POST"]) @login_required def add_app_port(app_id): """Add a port to an application""" app = App.query.get_or_404(app_id) # Check if the user owns this app if app.user_id != current_user.id: flash("Unauthorized access", "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) # Get port details from the form port_number = request.form.get("port_number") protocol = request.form.get("protocol", "TCP") description = request.form.get("description", "") # Validate the port valid, clean_port, error = validate_port_data( port_number, descriptions=description, server_id=app.server_id, exclude_app_id=app_id, protocol=protocol ) if not valid: flash(error, "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) try: # Check if the port already exists for this app existing_port = Port.query.filter_by( app_id=app_id, port_number=clean_port, protocol=protocol ).first() if existing_port: # Update the existing port description existing_port.description = description flash(f"Port {clean_port}/{protocol} updated", "success") else: # Create a new port port = Port( port_number=clean_port, protocol=protocol, description=description, app_id=app_id ) db.session.add(port) flash(f"Port {clean_port}/{protocol} added successfully", "success") db.session.commit() return redirect(url_for("dashboard.app_view", app_id=app_id)) except Exception as e: db.session.rollback() flash(f"Error adding port: {str(e)}", "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) @bp.route("/app//ports", methods=["GET"]) @login_required def get_app_ports(app_id): """Get all ports for an application""" app = App.query.get_or_404(app_id) ports = Port.query.filter_by(app_id=app_id).all() result = { "app_id": app_id, "ports": [ { "id": port.id, "port_number": port.port_number, "protocol": port.protocol, "description": port.description, } for port in ports ], } return jsonify(result) @bp.route("/app//port//delete", methods=["POST"]) @login_required def delete_app_port(app_id, port_id): """Delete a port from an application""" app = App.query.get_or_404(app_id) port = Port.query.get_or_404(port_id) if port.app_id != app.id: flash("Port does not belong to this application", "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) try: db.session.delete(port) db.session.commit() flash( f"Port {port.port_number}/{port.protocol} deleted successfully", "success" ) except Exception as e: db.session.rollback() flash(f"Error deleting port: {str(e)}", "danger") return redirect(url_for("dashboard.app_view", app_id=app_id)) @bp.route("/subnets//servers", methods=["GET"]) def get_subnet_servers(subnet_id): """Get all servers for a specific subnet""" servers = Server.query.filter_by(subnet_id=subnet_id).all() return jsonify( [ { "id": server.id, "hostname": server.hostname, "ip_address": server.ip_address, } for server in servers ] ) @bp.route("/server//ports", methods=["GET"]) @login_required def get_server_ports(server_id): """Get all used ports for a server""" server = Server.query.get_or_404(server_id) # Get all ports associated with this server ports = Port.query.filter_by(server_id=server_id).all() used_ports = [port.port_number for port in ports] return jsonify({"server_id": server_id, "used_ports": used_ports}) @bp.route("/server//free-port", methods=["GET"]) @login_required def get_free_port(server_id): """Find a free port for a server""" server = Server.query.get_or_404(server_id) # Get all ports associated with this server used_ports = [ port.port_number for port in Port.query.filter_by(server_id=server_id).all() ] # Find the first free port (starting from 8000) for port_number in range(8000, 9000): if port_number not in used_ports: return jsonify({"success": True, "port": port_number}) return jsonify( {"success": False, "error": "No free ports available in the range 8000-9000"} ) @bp.route("/validate/app-name", methods=["GET"]) @login_required def validate_app_name(): """API endpoint to validate application name in real-time with fuzzy matching""" name = request.args.get("name", "").strip() server_id = request.args.get("server_id") app_id = request.args.get("app_id") if not name or not server_id: return jsonify({"valid": False, "message": "Missing required parameters"}) # Check for existing app with same name query = App.query.filter(App.name == name, App.server_id == server_id) if app_id: query = query.filter(App.id != int(app_id)) existing_app = query.first() if existing_app: return jsonify({ "valid": False, "message": f"Application '{name}' already exists on this server", "app_id": existing_app.id, "edit_url": url_for('dashboard.app_edit', app_id=existing_app.id) }) # Get all apps on this server for similarity check server_apps = App.query.filter(App.server_id == server_id).all() # Filter out the current app if we're editing if app_id: server_apps = [app for app in server_apps if str(app.id) != app_id] # Find similar apps using fuzzy matching similar_apps = [] for app in server_apps: # Calculate similarity ratio using SequenceMatcher similarity = SequenceMatcher(None, name.lower(), app.name.lower()).ratio() # Consider similar if: # 1. One is a substring of the other OR # 2. They have high similarity ratio (over 0.7) is_substring = name.lower() in app.name.lower() or app.name.lower() in name.lower() is_similar = similarity > 0.7 if (is_substring or is_similar) and len(name) >= 3: similar_apps.append({ "name": app.name, "id": app.id, "similarity": similarity }) # Sort by similarity (highest first) and limit to top 3 similar_apps = sorted(similar_apps, key=lambda x: x["similarity"], reverse=True)[:3] if similar_apps: return jsonify({ "valid": True, "warning": "Similar application names found", "similar_apps": similar_apps }) return jsonify({"valid": True}) @bp.route("/validate/app-port", methods=["GET"]) @login_required def validate_app_port(): """API endpoint to validate port in real-time""" port_number = request.args.get("port_number", "").strip() protocol = request.args.get("protocol", "TCP") server_id = request.args.get("server_id") app_id = request.args.get("app_id") if not port_number or not server_id: return jsonify({"valid": False, "message": "Missing required parameters"}) try: clean_port = int(port_number) if clean_port < 1 or clean_port > 65535: return jsonify({"valid": False, "message": "Port must be between 1 and 65535"}) in_use, app_name = is_port_in_use( clean_port, protocol, server_id, exclude_app_id=app_id if app_id else None ) if in_use: conflict_app = App.query.filter_by(name=app_name, server_id=server_id).first() return jsonify({ "valid": False, "message": f"Port {clean_port}/{protocol} is already in use by application '{app_name}'", "app_id": conflict_app.id if conflict_app else None, "edit_url": url_for('dashboard.app_edit', app_id=conflict_app.id) if conflict_app else None }) return jsonify({"valid": True}) except ValueError: return jsonify({"valid": False, "message": "Invalid port number"}) @bp.route("/locations", methods=["POST"]) @login_required def create_location(): """API endpoint to create a new location""" data = request.json if not data or not data.get('name'): return jsonify({'error': 'Location name is required'}), 400 try: location = Location( name=data.get('name'), description=data.get('description', ''), user_id=current_user.id ) db.session.add(location) db.session.commit() return jsonify({ 'id': location.id, 'name': location.name, 'description': location.description }), 201 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @bp.route("/subnets", methods=["POST"]) @login_required def create_subnet(): """API endpoint to create a new subnet""" data = request.json if not data or not data.get('cidr') or not data.get('location_id'): return jsonify({'error': 'CIDR and location_id are required'}), 400 try: subnet = Subnet( cidr=data.get('cidr'), location_id=data.get('location_id'), user_id=current_user.id, auto_scan=data.get('auto_scan', False), active_hosts=json.dumps([]) ) db.session.add(subnet) db.session.commit() return jsonify({ 'id': subnet.id, 'cidr': subnet.cidr, 'location_id': subnet.location_id }), 201 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500