Files
StrikePackageGPT/services/kali-executor/app/main.py
mblanke b9428df6df feat: Add HackGpt Enterprise features
- 6-Phase pentest methodology UI (Recon, Scanning, Vuln, Exploit, Report, Retest)
- Phase-aware AI prompts with context from current phase
- Attack chain analysis and visualization
- CVSS-style severity badges (CRITICAL/HIGH/MEDIUM/LOW)
- Findings sidebar with severity counts
- Phase-specific tools and quick actions
2025-11-28 10:54:25 -05:00

481 lines
15 KiB
Python

"""
Kali Executor Service
Executes commands in the Kali container via Docker SDK.
"""
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
import docker
import asyncio
from concurrent.futures import ThreadPoolExecutor
import os
import uuid
import json
import re
from datetime import datetime
from contextlib import asynccontextmanager
# Allowed command prefixes (security whitelist)
ALLOWED_COMMANDS = {
# Reconnaissance
"nmap", "masscan", "amass", "theharvester", "whatweb", "dnsrecon", "fierce",
"dig", "nslookup", "host", "whois",
# Web testing
"nikto", "gobuster", "dirb", "sqlmap", "wpscan", "curl", "wget",
# Network utilities
"ping", "traceroute", "netcat", "nc", "tcpdump",
# Exploitation research
"searchsploit", "msfconsole", "msfvenom",
# Brute force
"hydra", "medusa",
# System info
"ls", "cat", "head", "tail", "grep", "find", "pwd", "whoami", "id",
"uname", "hostname", "ip", "ifconfig", "netstat", "ss",
# Python scripts
"python", "python3",
}
# Blocked patterns (dangerous commands)
BLOCKED_PATTERNS = [
r"rm\s+-rf\s+/", # Prevent recursive deletion of root
r"mkfs", # Prevent formatting
r"dd\s+if=", # Prevent disk operations
r">\s*/dev/", # Prevent writing to devices
r"chmod\s+777\s+/", # Prevent dangerous permission changes
r"shutdown", r"reboot", r"halt", # Prevent system control
r"kill\s+-9\s+-1", # Prevent killing all processes
]
def validate_command(command: str) -> tuple[bool, str]:
"""Validate command against whitelist and blocked patterns."""
# Get the base command (first word)
parts = command.strip().split()
if not parts:
return False, "Empty command"
base_cmd = parts[0].split("/")[-1] # Handle full paths
# Check blocked patterns first
for pattern in BLOCKED_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
return False, f"Blocked pattern detected: {pattern}"
# Check if command is in whitelist
if base_cmd not in ALLOWED_COMMANDS:
return False, f"Command '{base_cmd}' not in allowed list"
return True, "OK"
# Docker client
docker_client = None
kali_container = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifecycle manager for the FastAPI app."""
global docker_client, kali_container
try:
docker_client = docker.from_env()
kali_container = docker_client.containers.get(
os.getenv("KALI_CONTAINER_NAME", "strikepackage-kali")
)
print(f"Connected to Kali container: {kali_container.name}")
except docker.errors.NotFound:
print("Warning: Kali container not found. Command execution will fail.")
except docker.errors.DockerException as e:
print(f"Warning: Docker not available: {e}")
yield
if docker_client:
docker_client.close()
app = FastAPI(
title="Kali Executor",
description="Execute commands in the Kali container",
version="0.1.0",
lifespan=lifespan
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Store running commands
running_commands: Dict[str, Dict[str, Any]] = {}
class CommandRequest(BaseModel):
command: str
timeout: int = Field(default=300, ge=1, le=3600)
working_dir: str = "/workspace"
stream: bool = False
class CommandResult(BaseModel):
command_id: str
command: str
status: str
exit_code: Optional[int] = None
stdout: str = ""
stderr: str = ""
started_at: datetime
completed_at: Optional[datetime] = None
duration_seconds: Optional[float] = None
@app.get("/health")
async def health_check():
"""Health check endpoint."""
kali_status = "disconnected"
if kali_container:
try:
kali_container.reload()
kali_status = kali_container.status
except:
kali_status = "error"
return {
"status": "healthy",
"service": "kali-executor",
"kali_container": kali_status
}
@app.get("/processes")
async def get_running_processes():
"""Get list of running security tool processes in Kali container."""
global kali_container
if not kali_container:
raise HTTPException(status_code=503, detail="Kali container not available")
try:
# Get process list
loop = asyncio.get_event_loop()
exit_code, output = await loop.run_in_executor(
executor,
lambda: kali_container.exec_run(
cmd=["ps", "aux", "--sort=-start_time"],
demux=True
)
)
stdout = output[0].decode('utf-8', errors='replace') if output[0] else ""
# Parse processes and filter for security tools
security_tools = ["nmap", "nikto", "gobuster", "sqlmap", "hydra", "masscan",
"amass", "theharvester", "dirb", "wpscan", "searchsploit", "msfconsole"]
processes = []
for line in stdout.split('\n')[1:]: # Skip header
parts = line.split(None, 10)
if len(parts) >= 11:
cmd = parts[10]
pid = parts[1]
cpu = parts[2]
mem = parts[3]
time_running = parts[9]
# Check if it's a security tool
is_security_tool = any(tool in cmd.lower() for tool in security_tools)
if is_security_tool:
processes.append({
"pid": pid,
"cpu": cpu,
"mem": mem,
"time": time_running,
"command": cmd[:200] # Truncate long commands
})
return {
"running_processes": processes,
"count": len(processes)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Thread pool for blocking Docker operations
executor = ThreadPoolExecutor(max_workers=10)
def _run_command_sync(container, command, working_dir):
"""Synchronous command execution for thread pool."""
full_command = f"cd {working_dir} && {command}"
return container.exec_run(
cmd=["bash", "-c", full_command],
demux=True,
workdir=working_dir
)
@app.post("/execute", response_model=CommandResult)
async def execute_command(request: CommandRequest):
"""Execute a command in the Kali container."""
global kali_container
if not kali_container:
raise HTTPException(status_code=503, detail="Kali container not available")
# Validate command against whitelist
is_valid, message = validate_command(request.command)
if not is_valid:
raise HTTPException(status_code=403, detail=f"Command blocked: {message}")
command_id = str(uuid.uuid4())
started_at = datetime.utcnow()
try:
# Refresh container state
loop = asyncio.get_event_loop()
await loop.run_in_executor(executor, kali_container.reload)
if kali_container.status != "running":
raise HTTPException(status_code=503, detail="Kali container is not running")
# Execute command in thread pool to avoid blocking
exit_code, output = await loop.run_in_executor(
executor,
_run_command_sync,
kali_container,
request.command,
request.working_dir
)
completed_at = datetime.utcnow()
duration = (completed_at - started_at).total_seconds()
stdout = output[0].decode('utf-8', errors='replace') if output[0] else ""
stderr = output[1].decode('utf-8', errors='replace') if output[1] else ""
return CommandResult(
command_id=command_id,
command=request.command,
status="completed",
exit_code=exit_code,
stdout=stdout,
stderr=stderr,
started_at=started_at,
completed_at=completed_at,
duration_seconds=duration
)
except docker.errors.APIError as e:
raise HTTPException(status_code=500, detail=f"Docker error: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Execution error: {str(e)}")
@app.post("/execute/async")
async def execute_command_async(request: CommandRequest):
"""Execute a command asynchronously and return immediately."""
global kali_container
if not kali_container:
raise HTTPException(status_code=503, detail="Kali container not available")
# Validate command against whitelist
is_valid, message = validate_command(request.command)
if not is_valid:
raise HTTPException(status_code=403, detail=f"Command blocked: {message}")
command_id = str(uuid.uuid4())
started_at = datetime.utcnow()
running_commands[command_id] = {
"command": request.command,
"status": "running",
"started_at": started_at,
"stdout": "",
"stderr": ""
}
# Start background execution
asyncio.create_task(_run_command_background(
command_id, request.command, request.working_dir, request.timeout
))
return {"command_id": command_id, "status": "running"}
async def _run_command_background(command_id: str, command: str, working_dir: str, timeout: int):
"""Run command in background."""
global kali_container
try:
kali_container.reload()
full_command = f"cd {working_dir} && timeout {timeout} {command}"
exit_code, output = kali_container.exec_run(
cmd=["bash", "-c", full_command],
demux=True,
workdir=working_dir
)
running_commands[command_id].update({
"status": "completed",
"exit_code": exit_code,
"stdout": output[0].decode('utf-8', errors='replace') if output[0] else "",
"stderr": output[1].decode('utf-8', errors='replace') if output[1] else "",
"completed_at": datetime.utcnow()
})
except Exception as e:
running_commands[command_id].update({
"status": "failed",
"error": str(e),
"completed_at": datetime.utcnow()
})
@app.get("/execute/{command_id}")
async def get_command_status(command_id: str):
"""Get status of an async command."""
if command_id not in running_commands:
raise HTTPException(status_code=404, detail="Command not found")
return running_commands[command_id]
@app.websocket("/ws/execute")
async def websocket_execute(websocket: WebSocket):
"""WebSocket endpoint for streaming command output."""
global kali_container
await websocket.accept()
try:
while True:
data = await websocket.receive_json()
command = data.get("command")
working_dir = data.get("working_dir", "/workspace")
if not command:
await websocket.send_json({"error": "No command provided"})
continue
# Validate command against whitelist
is_valid, message = validate_command(command)
if not is_valid:
await websocket.send_json({"error": f"Command blocked: {message}"})
continue
if not kali_container:
await websocket.send_json({"error": "Kali container not available"})
continue
try:
kali_container.reload()
# Use exec_run with stream=True for real-time output
exec_result = kali_container.exec_run(
cmd=["bash", "-c", f"cd {working_dir} && {command}"],
stream=True,
demux=True,
workdir=working_dir
)
# Stream output
for stdout, stderr in exec_result.output:
if stdout:
await websocket.send_json({
"type": "stdout",
"data": stdout.decode('utf-8', errors='replace')
})
if stderr:
await websocket.send_json({
"type": "stderr",
"data": stderr.decode('utf-8', errors='replace')
})
await websocket.send_json({
"type": "complete",
"exit_code": exec_result.exit_code if hasattr(exec_result, 'exit_code') else 0
})
except Exception as e:
await websocket.send_json({"type": "error", "message": str(e)})
except WebSocketDisconnect:
pass
@app.get("/container/info")
async def get_container_info():
"""Get Kali container information."""
global kali_container
if not kali_container:
raise HTTPException(status_code=503, detail="Kali container not available")
try:
kali_container.reload()
return {
"id": kali_container.short_id,
"name": kali_container.name,
"status": kali_container.status,
"image": kali_container.image.tags[0] if kali_container.image.tags else "unknown",
"created": kali_container.attrs.get("Created"),
"network": list(kali_container.attrs.get("NetworkSettings", {}).get("Networks", {}).keys())
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/tools")
async def list_installed_tools():
"""List security tools installed in Kali container."""
global kali_container
if not kali_container:
raise HTTPException(status_code=503, detail="Kali container not available")
tools_to_check = [
"nmap", "masscan", "nikto", "sqlmap", "gobuster", "dirb",
"hydra", "amass", "theharvester", "whatweb", "wpscan",
"searchsploit", "msfconsole", "netcat", "curl", "wget"
]
installed = []
for tool in tools_to_check:
try:
exit_code, _ = kali_container.exec_run(
cmd=["which", tool],
demux=True
)
if exit_code == 0:
installed.append(tool)
except:
pass
return {"installed_tools": installed}
@app.get("/allowed-commands")
async def get_allowed_commands():
"""Get list of allowed commands for security validation."""
return {
"allowed_commands": sorted(list(ALLOWED_COMMANDS)),
"blocked_patterns": BLOCKED_PATTERNS
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8002)