626 lines
18 KiB
Python
626 lines
18 KiB
Python
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/<int:subnet_id>", 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/<int:subnet_id>/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/<int:server_id>", 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/<int:app_id>", 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/<int:server_id>/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/<int:app_id>/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/<int:app_id>/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/<int:app_id>/port/<int:port_id>/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/<int:subnet_id>/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/<int:server_id>/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/<int:server_id>/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
|