All checks were successful
Build Container / build (push) Successful in 1m18s
- Flask backend with SSH discovery and Claude AI integration - React/Vite frontend with Tailwind CSS - Docker multi-stage build - Gitea Actions workflow for container builds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
206 lines
6.3 KiB
Python
206 lines
6.3 KiB
Python
from dataclasses import dataclass
|
|
from typing import Optional, Callable
|
|
import uuid
|
|
|
|
from services.ssh_manager import get_ssh_manager, CommandResult
|
|
|
|
|
|
# Built-in safe commands that never require approval
|
|
SAFE_DISCOVERY_COMMANDS = {
|
|
"system_info": "uname -a",
|
|
"os_release": "cat /etc/os-release 2>/dev/null || cat /etc/*-release 2>/dev/null | head -20",
|
|
"docker_containers": "docker ps --format '{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Ports}}' 2>/dev/null || echo 'Docker not available'",
|
|
"systemd_services": "systemctl list-units --type=service --state=running --no-pager 2>/dev/null | head -50 || echo 'Systemd not available'",
|
|
"disk_usage": "df -h 2>/dev/null | head -20",
|
|
"memory_usage": "free -h 2>/dev/null || cat /proc/meminfo 2>/dev/null | head -10",
|
|
"cpu_count": "nproc 2>/dev/null || grep -c processor /proc/cpuinfo 2>/dev/null || echo 'Unknown'",
|
|
"open_ports": "ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null | head -30 || echo 'Unable to list ports'",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class DiscoveryResult:
|
|
"""Results from a host discovery scan."""
|
|
|
|
scan_id: str
|
|
hostname: str
|
|
username: str
|
|
port: int
|
|
connected: bool
|
|
error: Optional[str] = None
|
|
system_info: str = ""
|
|
os_release: str = ""
|
|
docker_containers: str = ""
|
|
systemd_services: str = ""
|
|
disk_usage: str = ""
|
|
memory_usage: str = ""
|
|
cpu_count: str = ""
|
|
open_ports: str = ""
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for JSON serialization."""
|
|
return {
|
|
"scan_id": self.scan_id,
|
|
"hostname": self.hostname,
|
|
"username": self.username,
|
|
"port": self.port,
|
|
"connected": self.connected,
|
|
"error": self.error,
|
|
"system_info": self.system_info,
|
|
"os_release": self.os_release,
|
|
"docker_containers": self.docker_containers,
|
|
"systemd_services": self.systemd_services,
|
|
"disk_usage": self.disk_usage,
|
|
"memory_usage": self.memory_usage,
|
|
"cpu_count": self.cpu_count,
|
|
"open_ports": self.open_ports,
|
|
}
|
|
|
|
|
|
class DiscoveryService:
|
|
"""Service for discovering services on remote hosts."""
|
|
|
|
def __init__(self):
|
|
self.active_scans: dict[str, DiscoveryResult] = {}
|
|
|
|
def is_safe_command(self, command: str) -> bool:
|
|
"""Check if a command is in the safe built-in list."""
|
|
# Check exact matches
|
|
if command in SAFE_DISCOVERY_COMMANDS.values():
|
|
return True
|
|
|
|
# Check if it's a safe read-only command pattern
|
|
safe_patterns = [
|
|
"cat ",
|
|
"head ",
|
|
"tail ",
|
|
"grep ",
|
|
"ls ",
|
|
"ps ",
|
|
"df ",
|
|
"free ",
|
|
"uname ",
|
|
"uptime",
|
|
"hostname",
|
|
"whoami",
|
|
"id ",
|
|
"docker ps",
|
|
"docker inspect",
|
|
"docker logs",
|
|
"systemctl status",
|
|
"systemctl list-",
|
|
"journalctl",
|
|
"ss ",
|
|
"netstat ",
|
|
"curl -s",
|
|
"wget -q",
|
|
"nproc",
|
|
]
|
|
|
|
command_lower = command.lower().strip()
|
|
for pattern in safe_patterns:
|
|
if command_lower.startswith(pattern):
|
|
return True
|
|
|
|
return False
|
|
|
|
def scan_host(
|
|
self,
|
|
hostname: str,
|
|
username: str = "root",
|
|
port: int = 22,
|
|
on_progress: Optional[Callable[[str, str], None]] = None,
|
|
) -> DiscoveryResult:
|
|
"""
|
|
Scan a host for services and system information.
|
|
|
|
Args:
|
|
hostname: Target hostname or IP
|
|
username: SSH username
|
|
port: SSH port
|
|
on_progress: Callback for progress updates (command_name, status)
|
|
|
|
Returns:
|
|
DiscoveryResult with all gathered information
|
|
"""
|
|
scan_id = str(uuid.uuid4())
|
|
result = DiscoveryResult(
|
|
scan_id=scan_id,
|
|
hostname=hostname,
|
|
username=username,
|
|
port=port,
|
|
connected=False,
|
|
)
|
|
self.active_scans[scan_id] = result
|
|
|
|
ssh = get_ssh_manager()
|
|
|
|
# Connect to host
|
|
if on_progress:
|
|
on_progress("connect", "connecting")
|
|
|
|
try:
|
|
ssh.connect(hostname, username, port)
|
|
result.connected = True
|
|
if on_progress:
|
|
on_progress("connect", "connected")
|
|
except Exception as e:
|
|
result.error = str(e)
|
|
if on_progress:
|
|
on_progress("connect", f"failed: {str(e)}")
|
|
return result
|
|
|
|
# Run discovery commands
|
|
for cmd_name, command in SAFE_DISCOVERY_COMMANDS.items():
|
|
if on_progress:
|
|
on_progress(cmd_name, "running")
|
|
|
|
try:
|
|
cmd_result = ssh.execute(hostname, command, username, port, timeout=30)
|
|
output = cmd_result.stdout if cmd_result.success else cmd_result.stderr
|
|
setattr(result, cmd_name, output.strip())
|
|
|
|
if on_progress:
|
|
on_progress(cmd_name, "complete" if cmd_result.success else "failed")
|
|
except Exception as e:
|
|
setattr(result, cmd_name, f"Error: {str(e)}")
|
|
if on_progress:
|
|
on_progress(cmd_name, f"error: {str(e)}")
|
|
|
|
return result
|
|
|
|
def run_additional_command(
|
|
self,
|
|
hostname: str,
|
|
command: str,
|
|
username: str = "root",
|
|
port: int = 22,
|
|
) -> CommandResult:
|
|
"""
|
|
Run an additional command on a host.
|
|
|
|
This should only be called after approval if in dev mode.
|
|
"""
|
|
ssh = get_ssh_manager()
|
|
|
|
if not ssh.is_connected(hostname, username, port):
|
|
ssh.connect(hostname, username, port)
|
|
|
|
return ssh.execute(hostname, command, username, port, timeout=60)
|
|
|
|
def get_scan(self, scan_id: str) -> Optional[DiscoveryResult]:
|
|
"""Get a scan result by ID."""
|
|
return self.active_scans.get(scan_id)
|
|
|
|
|
|
# Global discovery service instance
|
|
_discovery_service: Optional[DiscoveryService] = None
|
|
|
|
|
|
def get_discovery_service() -> DiscoveryService:
|
|
"""Get the global discovery service instance."""
|
|
global _discovery_service
|
|
if _discovery_service is None:
|
|
_discovery_service = DiscoveryService()
|
|
return _discovery_service
|