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
This commit is contained in:
2025-11-28 10:54:25 -05:00
parent 8b89e27b68
commit b9428df6df
32 changed files with 4641 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
COPY templates/ ./templates/
COPY static/ ./static/
# Expose port
EXPOSE 8080
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

View File

View File

@@ -0,0 +1,368 @@
"""
StrikePackageGPT Dashboard
Web interface for security analysis and LLM-powered penetration testing assistant.
"""
from fastapi import FastAPI, Request, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
import httpx
import os
import json
app = FastAPI(
title="StrikePackageGPT Dashboard",
description="Web interface for AI-powered security analysis",
version="0.2.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configuration
HACKGPT_API_URL = os.getenv("HACKGPT_API_URL", "http://strikepackage-hackgpt-api:8001")
LLM_ROUTER_URL = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
KALI_EXECUTOR_URL = os.getenv("KALI_EXECUTOR_URL", "http://strikepackage-kali-executor:8002")
# Static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# Templates
templates = Jinja2Templates(directory="templates")
class ChatMessage(BaseModel):
message: str
session_id: Optional[str] = None
provider: str = "ollama"
model: str = "llama3.2"
context: Optional[str] = None
class PhaseChatMessage(BaseModel):
message: str
phase: str
provider: str = "ollama"
model: str = "llama3.2"
findings: List[Dict[str, Any]] = Field(default_factory=list)
class AttackChainRequest(BaseModel):
findings: List[Dict[str, Any]]
provider: str = "ollama"
model: str = "llama3.2"
class CommandRequest(BaseModel):
command: str
timeout: int = Field(default=300, ge=1, le=3600)
working_dir: str = "/workspace"
class ScanRequest(BaseModel):
tool: str
target: str
scan_type: Optional[str] = None
options: Dict[str, Any] = Field(default_factory=dict)
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "dashboard"}
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
"""Main dashboard page"""
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/terminal", response_class=HTMLResponse)
async def terminal_page(request: Request):
"""Terminal page"""
return templates.TemplateResponse("terminal.html", {"request": request})
@app.get("/api/status")
async def get_services_status():
"""Get status of all backend services"""
services = {}
service_checks = [
("llm-router", f"{LLM_ROUTER_URL}/health"),
("hackgpt-api", f"{HACKGPT_API_URL}/health"),
("kali-executor", f"{KALI_EXECUTOR_URL}/health"),
]
async with httpx.AsyncClient() as client:
for name, url in service_checks:
try:
response = await client.get(url, timeout=5.0)
services[name] = response.status_code == 200
except:
services[name] = False
return {"services": services}
@app.get("/api/processes")
async def get_running_processes():
"""Get running security processes in Kali container"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{KALI_EXECUTOR_URL}/processes", timeout=10.0)
if response.status_code == 200:
return response.json()
return {"running_processes": [], "count": 0}
except:
return {"running_processes": [], "count": 0}
@app.get("/api/providers")
async def get_providers():
"""Get available LLM providers"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{LLM_ROUTER_URL}/providers", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail="Failed to get providers")
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="LLM Router not available")
@app.get("/api/tools")
async def get_tools():
"""Get available security tools"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{HACKGPT_API_URL}/tools", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail="Failed to get tools")
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/chat")
async def chat(message: ChatMessage):
"""Send chat message to HackGPT API"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/chat",
json=message.model_dump(),
timeout=120.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/chat/phase")
async def phase_chat(message: PhaseChatMessage):
"""Send phase-aware chat message to HackGPT API"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/chat/phase",
json=message.model_dump(),
timeout=120.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/attack-chains")
async def analyze_attack_chains(request: AttackChainRequest):
"""Analyze findings to identify attack chains"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/attack-chains",
json=request.model_dump(),
timeout=120.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/analyze")
async def analyze(request: Request):
"""Start security analysis"""
data = await request.json()
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/analyze",
json=data,
timeout=30.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.get("/api/task/{task_id}")
async def get_task(task_id: str):
"""Get task status"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{HACKGPT_API_URL}/task/{task_id}", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/suggest-command")
async def suggest_command(message: ChatMessage):
"""Get AI-suggested security commands"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/suggest-command",
json=message.model_dump(),
timeout=60.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
# ============== Command Execution ==============
@app.post("/api/execute")
async def execute_command(request: CommandRequest):
"""Execute a command in the Kali container"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/execute",
json=request.model_dump(),
timeout=float(request.timeout + 30)
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Command execution timed out")
# ============== Scan Management ==============
@app.post("/api/scan")
async def start_scan(request: ScanRequest):
"""Start a security scan"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/scan",
json=request.model_dump(),
timeout=30.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.get("/api/scan/{scan_id}")
async def get_scan_result(scan_id: str):
"""Get scan results"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{HACKGPT_API_URL}/scan/{scan_id}", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.get("/api/scans")
async def list_scans():
"""List all scans"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{HACKGPT_API_URL}/scans", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
@app.post("/api/ai-scan")
async def ai_scan(message: ChatMessage):
"""AI-assisted scanning"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{HACKGPT_API_URL}/ai-scan",
json=message.model_dump(),
timeout=120.0
)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="HackGPT API not available")
# ============== Kali Container Info ==============
@app.get("/api/kali/info")
async def get_kali_info():
"""Get Kali container information"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{KALI_EXECUTOR_URL}/container/info", timeout=10.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Kali executor not available")
@app.get("/api/kali/tools")
async def get_kali_tools():
"""Get installed tools in Kali container"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{KALI_EXECUTOR_URL}/tools", timeout=30.0)
if response.status_code == 200:
return response.json()
raise HTTPException(status_code=response.status_code, detail=response.text)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Kali executor not available")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@@ -0,0 +1,5 @@
fastapi==0.115.5
uvicorn[standard]==0.32.1
httpx==0.28.1
pydantic==2.10.2
jinja2==3.1.4

View File

@@ -0,0 +1,8 @@
# Static Assets
Place your custom assets here:
- `icon.png` - Your StrikePackageGPT logo/icon (recommended: 64x64 or 128x128)
- `flag.png` - Canadian flag image (recommended: ~32px height)
These will appear in the dashboard header.

View File

@@ -0,0 +1,946 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>StrikePackageGPT - Security Analysis Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="icon" type="image/png" href="/static/icon.png">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'sp-black': '#0a0a0a',
'sp-dark': '#141414',
'sp-grey': '#1f1f1f',
'sp-grey-light': '#2a2a2a',
'sp-grey-mid': '#3a3a3a',
'sp-red': '#dc2626',
'sp-red-dark': '#991b1b',
'sp-red-light': '#ef4444',
'sp-white': '#ffffff',
'sp-white-dim': '#e5e5e5',
'sp-white-muted': '#a3a3a3',
'sev-critical': '#dc2626',
'sev-high': '#ea580c',
'sev-medium': '#eab308',
'sev-low': '#22c55e',
'sev-info': '#3b82f6',
}
}
}
}
</script>
<style>
.chat-container { height: calc(100vh - 320px); }
.terminal-container { height: calc(100vh - 280px); }
.message-content pre { background: #0a0a0a; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; font-family: monospace; border: 1px solid #2a2a2a; }
.message-content code { background: #1f1f1f; padding: 0.2rem 0.4rem; border-radius: 0.25rem; font-family: monospace; color: #ef4444; }
.typing-indicator span { animation: blink 1.4s infinite both; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0; } 30% { opacity: 1; } }
.terminal-output { font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; font-size: 13px; line-height: 1.5; }
.terminal-output .stdout { color: #e5e5e5; }
.terminal-output .stderr { color: #ef4444; }
.terminal-output .cmd { color: #dc2626; }
.scan-card { transition: all 0.2s ease; }
.scan-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15); }
.glow-red { box-shadow: 0 0 20px rgba(220, 38, 38, 0.3); }
@keyframes pulse-red { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.pulse-red { animation: pulse-red 2s ease-in-out infinite; }
.severity-badge { font-weight: 600; padding: 2px 8px; border-radius: 4px; font-size: 11px; text-transform: uppercase; }
</style>
</head>
<body class="bg-sp-black text-sp-white">
<div x-data="dashboard()" x-init="init()" class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-sp-dark border-b border-sp-grey-mid px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="flex items-center gap-3">
<img src="/static/icon.png" alt="StrikePackageGPT" class="h-10 w-10" onerror="this.style.display='none'">
<div>
<h1 class="text-2xl font-bold text-sp-red">StrikePackageGPT</h1>
<span class="text-xs text-sp-white-muted">AI-Powered Penetration Testing Platform</span>
</div>
</div>
<img src="/static/flag.png" alt="Canada" class="h-6 ml-2" onerror="this.style.display='none'">
</div>
<div class="flex items-center gap-4">
<div x-show="runningProcesses.length > 0"
class="flex items-center gap-2 px-3 py-1.5 rounded bg-sp-red/20 border border-sp-red/50 cursor-pointer"
@click="showProcesses = !showProcesses">
<span class="w-2 h-2 rounded-full bg-sp-red pulse-red"></span>
<span class="text-sp-red text-sm font-medium" x-text="runningProcesses.length + ' Running'"></span>
</div>
<div class="flex items-center gap-3 text-sm">
<template x-for="(status, service) in services" :key="service">
<div class="flex items-center gap-1 px-2 py-1 rounded bg-sp-grey">
<span class="w-2 h-2 rounded-full" :class="status ? 'bg-green-500' : 'bg-sp-red'"></span>
<span class="text-sp-white-muted text-xs" x-text="service"></span>
</div>
</template>
</div>
<select x-model="selectedProvider" @change="updateModels()"
class="bg-sp-grey border border-sp-grey-mid rounded px-3 py-1 text-sm text-sp-white focus:border-sp-red focus:outline-none">
<option value="ollama">Ollama (Local)</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
</select>
<select x-model="selectedModel" class="bg-sp-grey border border-sp-grey-mid rounded px-3 py-1 text-sm text-sp-white focus:border-sp-red focus:outline-none">
<template x-for="model in availableModels" :key="model">
<option :value="model" x-text="model"></option>
</template>
</select>
</div>
</div>
<div x-show="showProcesses && runningProcesses.length > 0" x-transition
class="mt-4 p-3 bg-sp-grey rounded border border-sp-grey-mid">
<h4 class="text-sm font-semibold text-sp-red mb-2">🔄 Running Processes</h4>
<div class="space-y-1">
<template x-for="proc in runningProcesses" :key="proc.pid">
<div class="flex items-center justify-between text-xs bg-sp-dark p-2 rounded">
<span class="text-sp-white-muted font-mono" x-text="proc.command"></span>
<div class="flex items-center gap-3">
<span class="text-sp-white-muted">CPU: <span class="text-sp-red" x-text="proc.cpu + '%'"></span></span>
<span class="text-sp-white-muted">Time: <span x-text="proc.time"></span></span>
</div>
</div>
</template>
</div>
</div>
<!-- 6-Phase Methodology Tabs -->
<div class="flex gap-1 mt-4 border-b border-sp-grey-mid">
<template x-for="phase in phases" :key="phase.id">
<button @click="activePhase = phase.id; activeTab = 'phase'"
:class="activePhase === phase.id && activeTab === 'phase' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
<span x-text="phase.icon"></span>
<span class="hidden lg:inline" x-text="phase.name"></span>
<span class="lg:hidden" x-text="phase.short"></span>
</button>
</template>
<div class="flex-1"></div>
<button @click="activeTab = 'terminal'"
:class="activeTab === 'terminal' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2">
🖥️ Terminal
</button>
<button @click="activeTab = 'attack-chains'"
:class="activeTab === 'attack-chains' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
⛓️ Attack Chains
<span x-show="attackChains.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="attackChains.length"></span>
</button>
</div>
</header>
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-sp-dark border-r border-sp-grey-mid p-4 overflow-y-auto">
<div class="mb-4 p-3 bg-sp-grey rounded border border-sp-grey-mid">
<h3 class="text-sm font-semibold text-sp-red mb-2" x-text="getCurrentPhase().name"></h3>
<p class="text-xs text-sp-white-muted" x-text="getCurrentPhase().description"></p>
</div>
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">🛠️ Phase Tools</h2>
<div class="space-y-1 mb-6">
<template x-for="tool in getCurrentPhase().tools" :key="tool.name">
<button @click="askAboutTool(tool)"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded transition">
<span class="font-mono text-sp-red-light" x-text="tool.name"></span>
<p class="text-xs text-sp-white-muted truncate" x-text="tool.description"></p>
</button>
</template>
</div>
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">⚡ Quick Actions</h2>
<div class="space-y-2">
<template x-for="action in getCurrentPhase().quickActions" :key="action.label">
<button @click="executeQuickAction(action)"
class="w-full text-left text-sm bg-sp-grey hover:bg-sp-grey-light hover:border-sp-red border border-transparent px-3 py-2 rounded flex items-center gap-2 transition">
<span x-text="action.icon"></span>
<span x-text="action.label"></span>
</button>
</template>
</div>
<div class="mt-6" x-show="findings.length > 0">
<h2 class="text-lg font-semibold mb-4 text-sp-white-dim">📊 Findings</h2>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-sev-critical">Critical</span>
<span class="font-bold" x-text="countBySeverity('critical')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-high">High</span>
<span class="font-bold" x-text="countBySeverity('high')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-medium">Medium</span>
<span class="font-bold" x-text="countBySeverity('medium')"></span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-sev-low">Low</span>
<span class="font-bold" x-text="countBySeverity('low')"></span>
</div>
</div>
</div>
</aside>
<!-- Content Area -->
<main class="flex-1 flex flex-col">
<!-- Phase Content -->
<div x-show="activeTab === 'phase'" class="flex-1 flex flex-col">
<div class="chat-container overflow-y-auto p-6 space-y-4" id="chatContainer">
<template x-for="(msg, index) in messages" :key="index">
<div :class="msg.role === 'user' ? 'flex justify-end' : 'flex justify-start'">
<div :class="msg.role === 'user'
? 'bg-sp-red text-white max-w-3xl'
: 'bg-sp-grey text-sp-white-dim max-w-4xl border border-sp-grey-mid'"
class="rounded-lg px-4 py-3 message-content">
<template x-if="msg.role === 'assistant' && msg.phase">
<div class="flex items-center gap-2 mb-2 pb-2 border-b border-sp-grey-mid">
<span class="text-xs px-2 py-1 bg-sp-red/20 text-sp-red rounded" x-text="msg.phase"></span>
<template x-if="msg.risk_score">
<span :class="getRiskColor(msg.risk_score)"
class="severity-badge"
x-text="getRiskLabel(msg.risk_score)"></span>
</template>
</div>
</template>
<div x-html="renderMarkdown(msg.content)"></div>
<template x-if="msg.findings && msg.findings.length > 0">
<div class="mt-3 pt-3 border-t border-sp-grey-mid">
<h5 class="text-xs font-semibold text-sp-white-muted mb-2">Findings:</h5>
<div class="space-y-1">
<template x-for="finding in msg.findings" :key="finding.id">
<div class="flex items-center gap-2 text-xs bg-sp-black/50 p-2 rounded">
<span :class="getSeverityBadgeClass(finding.severity)"
class="severity-badge"
x-text="finding.severity"></span>
<span x-text="finding.title"></span>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<div x-show="isLoading" class="flex justify-start">
<div class="bg-sp-grey rounded-lg px-4 py-3 border border-sp-grey-mid">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
<span class="w-2 h-2 bg-sp-red rounded-full"></span>
</div>
</div>
</div>
</div>
<div x-show="phaseScans.length > 0" class="border-t border-sp-grey-mid bg-sp-dark p-3">
<div class="flex items-center gap-4 overflow-x-auto">
<span class="text-xs text-sp-white-muted font-semibold">Scans:</span>
<template x-for="scan in phaseScans" :key="scan.scan_id">
<div @click="viewScanDetails(scan)"
class="flex items-center gap-2 px-3 py-1 bg-sp-grey rounded cursor-pointer hover:bg-sp-grey-light">
<span :class="{
'bg-yellow-500': scan.status === 'running' || scan.status === 'pending',
'bg-green-500': scan.status === 'completed',
'bg-sp-red': scan.status === 'failed'
}" class="w-2 h-2 rounded-full"></span>
<span class="text-xs text-sp-red font-mono" x-text="scan.tool"></span>
<span class="text-xs text-sp-white-muted" x-text="scan.target"></span>
</div>
</template>
</div>
</div>
<div class="border-t border-sp-grey-mid p-4 bg-sp-dark">
<form @submit.prevent="sendMessage()" class="flex gap-4">
<input type="text" x-model="userInput"
:placeholder="getPhasePrompt()"
class="flex-1 bg-sp-grey border border-sp-grey-mid rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-sp-red focus:border-transparent text-sp-white placeholder-sp-white-muted"
:disabled="isLoading">
<button type="submit"
class="bg-sp-red hover:bg-sp-red-dark disabled:bg-sp-grey-mid px-6 py-3 rounded-lg font-semibold transition text-white"
:disabled="isLoading || !userInput.trim()">
Send
</button>
</form>
</div>
</div>
<!-- Terminal Tab -->
<div x-show="activeTab === 'terminal'" class="flex-1 flex flex-col">
<div class="terminal-container overflow-y-auto p-4 bg-sp-black font-mono" id="terminalOutput">
<div class="terminal-output">
<template x-for="(line, index) in terminalHistory" :key="index">
<div>
<template x-if="line.type === 'cmd'">
<div class="cmd mb-1">
<span class="text-sp-red">kali@strikepackage</span>:<span class="text-sp-white-muted">~</span>$ <span class="text-white" x-text="line.content"></span>
</div>
</template>
<template x-if="line.type === 'stdout'">
<pre class="stdout whitespace-pre-wrap" x-text="line.content"></pre>
</template>
<template x-if="line.type === 'stderr'">
<pre class="stderr whitespace-pre-wrap" x-text="line.content"></pre>
</template>
<template x-if="line.type === 'info'">
<div class="text-sp-white-muted italic" x-text="line.content"></div>
</template>
</div>
</template>
<div x-show="terminalLoading" class="text-sp-red animate-pulse">
Executing command...
</div>
</div>
</div>
<div class="border-t border-sp-grey-mid p-4 bg-sp-dark">
<form @submit.prevent="executeCommand()" class="flex gap-4">
<div class="flex items-center text-sp-red font-mono text-sm">
kali@strikepackage:~$
</div>
<input type="text" x-model="terminalInput"
placeholder="Enter command to execute in Kali container..."
class="flex-1 bg-sp-black border border-sp-grey-mid rounded px-4 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted"
:disabled="terminalLoading"
@keyup.up="historyUp()"
@keyup.down="historyDown()">
<button type="submit"
class="bg-sp-red hover:bg-sp-red-dark disabled:bg-sp-grey-mid px-4 py-2 rounded font-semibold transition text-white"
:disabled="terminalLoading || !terminalInput.trim()">
Run
</button>
</form>
</div>
</div>
<!-- Attack Chains Tab -->
<div x-show="activeTab === 'attack-chains'" class="flex-1 overflow-y-auto p-6">
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-xl font-bold text-sp-white">⛓️ Attack Chain Analysis</h2>
<p class="text-sm text-sp-white-muted">Correlated vulnerabilities and potential attack paths</p>
</div>
<button @click="analyzeAttackChains()"
class="bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center gap-2"
:disabled="findings.length === 0">
🔗 Analyze Chains
</button>
</div>
<div x-show="attackChains.length === 0" class="text-center text-sp-white-muted py-12">
<p class="text-4xl mb-4">⛓️</p>
<p>No attack chains detected yet.</p>
<p class="text-sm mt-2">Complete reconnaissance and vulnerability scanning to identify potential attack paths.</p>
</div>
<div class="space-y-6">
<template x-for="(chain, chainIdx) in attackChains" :key="chainIdx">
<div class="bg-sp-dark rounded-lg border border-sp-grey-mid overflow-hidden">
<div class="p-4 border-b border-sp-grey-mid flex items-center justify-between">
<div class="flex items-center gap-3">
<span :class="getRiskColor(chain.risk_score)"
class="severity-badge"
x-text="getRiskLabel(chain.risk_score)"></span>
<h3 class="font-bold text-sp-white" x-text="chain.name"></h3>
</div>
<div class="text-sm text-sp-white-muted">
Risk Score: <span class="text-sp-red font-bold" x-text="chain.risk_score.toFixed(1)"></span>/10
</div>
</div>
<div class="p-4">
<div class="relative">
<template x-for="(step, stepIdx) in chain.steps" :key="stepIdx">
<div class="flex items-start gap-4 mb-4 last:mb-0">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-sp-red flex items-center justify-center text-white font-bold text-sm">
<span x-text="step.step"></span>
</div>
<div class="flex-1 bg-sp-grey rounded p-3">
<div class="font-semibold text-sp-white" x-text="step.action"></div>
<div class="text-sm text-sp-white-muted mt-1" x-text="step.method || step.vuln || step.tools"></div>
</div>
</div>
</template>
</div>
<div class="mt-4 pt-4 border-t border-sp-grey-mid">
<div class="flex items-start gap-2">
<span class="text-sp-red">⚠️</span>
<div>
<span class="text-sm font-semibold text-sp-white-muted">Impact:</span>
<span class="text-sm text-sp-white" x-text="chain.impact"></span>
</div>
</div>
<div class="flex items-center gap-4 mt-2 text-sm text-sp-white-muted">
<span>Likelihood: <span class="text-sp-red" x-text="(chain.likelihood * 100).toFixed(0) + '%'"></span></span>
<span x-text="chain.recommendation"></span>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</main>
</div>
<!-- Scan Modal -->
<div x-show="scanModalOpen" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-md border border-sp-grey-mid glow-red" @click.away="scanModalOpen = false">
<h3 class="text-lg font-bold mb-4 text-sp-white">Start <span class="text-sp-red" x-text="scanModal.tool"></span> Scan</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-sp-white-muted mb-1">Target</label>
<input type="text" x-model="scanModal.target"
placeholder="IP, hostname, or URL"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sp-red text-sp-white placeholder-sp-white-muted">
</div>
<div>
<label class="block text-sm text-sp-white-muted mb-1">Scan Type</label>
<select x-model="scanModal.scanType"
class="w-full bg-sp-grey border border-sp-grey-mid rounded px-4 py-2 text-sp-white focus:outline-none focus:ring-2 focus:ring-sp-red">
<template x-for="type in scanModal.types" :key="type">
<option :value="type" x-text="type"></option>
</template>
</select>
</div>
</div>
<div class="flex gap-3 mt-6">
<button @click="scanModalOpen = false"
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded transition">
Cancel
</button>
<button @click="startScan()"
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded font-semibold transition"
:disabled="!scanModal.target">
Start Scan
</button>
</div>
</div>
</div>
<!-- Scan Details Modal -->
<div x-show="detailsModalOpen" x-transition class="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div class="bg-sp-dark rounded-lg p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto border border-sp-grey-mid" @click.away="detailsModalOpen = false">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-sp-white">Scan Details</h3>
<button @click="detailsModalOpen = false" class="text-sp-white-muted hover:text-white text-xl"></button>
</div>
<template x-if="selectedScan">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div><span class="text-sp-white-muted">Tool:</span> <span class="text-sp-red font-bold" x-text="selectedScan.tool"></span></div>
<div><span class="text-sp-white-muted">Target:</span> <span class="text-sp-white" x-text="selectedScan.target"></span></div>
<div><span class="text-sp-white-muted">Status:</span> <span x-text="selectedScan.status"></span></div>
<div><span class="text-sp-white-muted">Started:</span> <span x-text="selectedScan.started_at"></span></div>
</div>
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Command</h4>
<pre class="bg-sp-black p-3 rounded text-sm overflow-x-auto border border-sp-grey font-mono text-sp-white" x-text="selectedScan.command"></pre>
</div>
<template x-if="selectedScan.parsed && selectedScan.parsed.findings">
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Findings (<span x-text="selectedScan.parsed.findings.length"></span>)</h4>
<div class="space-y-2 max-h-48 overflow-y-auto">
<template x-for="finding in selectedScan.parsed.findings" :key="finding.raw">
<div class="flex items-start gap-2 bg-sp-black p-2 rounded text-sm">
<span :class="getSeverityBadgeClass(finding.severity)"
class="severity-badge flex-shrink-0"
x-text="finding.severity"></span>
<span class="text-sp-white-dim" x-text="finding.raw"></span>
</div>
</template>
</div>
</div>
</template>
<template x-if="selectedScan.result && selectedScan.result.stdout">
<div>
<h4 class="text-sm text-sp-white-muted mb-2">Raw Output</h4>
<pre class="bg-sp-black p-3 rounded text-sm overflow-x-auto text-sp-white-dim max-h-96 border border-sp-grey font-mono" x-text="selectedScan.result.stdout"></pre>
</div>
</template>
</div>
</template>
</div>
</div>
</div>
<script>
function dashboard() {
return {
activeTab: 'phase',
activePhase: 'recon',
messages: [],
userInput: '',
isLoading: false,
services: {},
selectedProvider: 'ollama',
selectedModel: 'llama3.2',
availableModels: ['llama3.2', 'codellama', 'mistral'],
providers: {},
terminalInput: '',
terminalHistory: [],
terminalLoading: false,
commandHistory: [],
historyIndex: -1,
scans: [],
scanModalOpen: false,
scanModal: { tool: '', target: '', scanType: '', types: [] },
detailsModalOpen: false,
selectedScan: null,
runningProcesses: [],
showProcesses: false,
findings: [],
attackChains: [],
phases: [
{
id: 'recon',
name: 'Reconnaissance',
short: 'Recon',
icon: '🔍',
description: 'Gather information about the target through passive and active reconnaissance techniques.',
tools: [
{ name: 'nmap', description: 'Network scanner and port enumeration' },
{ name: 'theHarvester', description: 'OSINT gathering for emails, subdomains' },
{ name: 'amass', description: 'Subdomain enumeration and mapping' },
{ name: 'whatweb', description: 'Web technology fingerprinting' },
],
quickActions: [
{ icon: '🎯', label: 'Quick Port Scan', tool: 'nmap', scanType: 'quick' },
{ icon: '🔎', label: 'Tech Fingerprint', tool: 'whatweb', scanType: 'default' },
]
},
{
id: 'scanning',
name: 'Scanning',
short: 'Scan',
icon: '📡',
description: 'Perform in-depth scanning and enumeration of discovered services.',
tools: [
{ name: 'nmap -sV', description: 'Service version detection' },
{ name: 'gobuster', description: 'Directory and file brute-force' },
{ name: 'enum4linux', description: 'SMB/Windows enumeration' },
],
quickActions: [
{ icon: '🔬', label: 'Full Port Scan', tool: 'nmap', scanType: 'full' },
{ icon: '📁', label: 'Dir Brute Force', tool: 'gobuster', scanType: 'dir' },
]
},
{
id: 'vuln',
name: 'Vulnerability Assessment',
short: 'Vuln',
icon: '🛡️',
description: 'Identify and assess vulnerabilities in discovered services.',
tools: [
{ name: 'nikto', description: 'Web server vulnerability scanner' },
{ name: 'nuclei', description: 'Template-based vuln scanner' },
{ name: 'sqlmap', description: 'SQL injection detection' },
],
quickActions: [
{ icon: '🕸️', label: 'Web Vuln Scan', tool: 'nikto', scanType: 'default' },
{ icon: '💉', label: 'SQLi Test', tool: 'sqlmap', scanType: 'test' },
{ icon: '🔎', label: 'Nmap Vuln Scripts', tool: 'nmap', scanType: 'vuln' },
]
},
{
id: 'exploit',
name: 'Exploitation',
short: 'Exploit',
icon: '💥',
description: 'Safely exploit verified vulnerabilities to demonstrate impact.',
tools: [
{ name: 'metasploit', description: 'Exploitation framework' },
{ name: 'hydra', description: 'Password brute-force tool' },
{ name: 'searchsploit', description: 'Exploit database search' },
],
quickActions: [
{ icon: '🔍', label: 'Search Exploits', prompt: 'Search for exploits for the vulnerabilities we found' },
]
},
{
id: 'report',
name: 'Reporting',
short: 'Report',
icon: '📄',
description: 'Document findings and create professional security reports.',
tools: [
{ name: 'Report Generator', description: 'AI-generated executive summary' },
{ name: 'CVSS Calculator', description: 'Severity scoring' },
],
quickActions: [
{ icon: '📊', label: 'Executive Summary', prompt: 'Generate an executive summary of our findings' },
{ icon: '📋', label: 'Technical Report', prompt: 'Create a detailed technical report' },
]
},
{
id: 'retest',
name: 'Retesting',
short: 'Retest',
icon: '🔄',
description: 'Verify that vulnerabilities have been properly remediated.',
tools: [
{ name: 'Verification Scan', description: 'Re-run previous scans' },
],
quickActions: [
{ icon: '✅', label: 'Verify Fixes', prompt: 'Create a retest plan for the identified vulnerabilities' },
]
}
],
async init() {
await this.checkStatus();
await this.checkRunningProcesses();
setInterval(() => this.checkStatus(), 30000);
setInterval(() => this.checkRunningProcesses(), 5000);
await this.loadProviders();
await this.refreshScans();
setInterval(() => this.pollRunningScans(), 5000);
this.messages.push({
role: 'assistant',
phase: 'Reconnaissance',
content: `# Welcome to StrikePackageGPT! 🍁
I'm your AI-powered penetration testing assistant, following the **6-Phase Enterprise Methodology**:
| Phase | Purpose |
|-------|---------|
| 🔍 **Reconnaissance** | OSINT, subdomain discovery, port scanning |
| 📡 **Scanning** | Service enumeration, version detection |
| 🛡️ **Vulnerability Assessment** | CVE identification, CVSS scoring |
| 💥 **Exploitation** | Safe, controlled exploitation |
| 📄 **Reporting** | Executive & technical reports |
| 🔄 **Retesting** | Verify remediation |
**New Features:**
- ⛓️ **Attack Chain Analysis** - Correlate vulnerabilities into attack paths
- 🎯 **CVSS Scoring** - Risk severity badges on all findings
- 🤖 **Context-Aware AI** - Prompts tailored to each phase
Select a phase above to begin, or use the quick actions in the sidebar!`
});
this.terminalHistory.push({
type: 'info',
content: 'Connected to Kali container. Type commands below to execute.'
});
},
getCurrentPhase() {
return this.phases.find(p => p.id === this.activePhase) || this.phases[0];
},
getPhasePrompt() {
const prompts = {
recon: 'Ask about reconnaissance techniques or specify a target to scan...',
scanning: 'Request service enumeration or detailed scanning...',
vuln: 'Ask about vulnerability assessment or analyze scan results...',
exploit: 'Discuss exploitation strategies or search for exploits...',
report: 'Request report generation or findings summary...',
retest: 'Plan retesting or verify remediation...'
};
return prompts[this.activePhase] || 'Ask me anything about security testing...';
},
get phaseScans() {
const phaseTools = {
recon: ['nmap', 'theHarvester', 'amass', 'whatweb', 'whois'],
scanning: ['nmap', 'masscan', 'enum4linux', 'gobuster'],
vuln: ['nikto', 'nuclei', 'sqlmap', 'searchsploit'],
exploit: ['hydra', 'metasploit'],
report: [],
retest: []
};
const tools = phaseTools[this.activePhase] || [];
return this.scans.filter(s => tools.includes(s.tool));
},
async checkStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
this.services = data.services;
} catch (e) { console.error('Failed to check status:', e); }
},
async checkRunningProcesses() {
try {
const response = await fetch('/api/processes');
const data = await response.json();
this.runningProcesses = data.running_processes || [];
} catch (e) { console.error('Failed to check processes:', e); }
},
async loadProviders() {
try {
const response = await fetch('/api/providers');
this.providers = await response.json();
this.updateModels();
} catch (e) { console.error('Failed to load providers:', e); }
},
updateModels() {
if (this.providers[this.selectedProvider]) {
this.availableModels = this.providers[this.selectedProvider].models || [];
this.selectedModel = this.availableModels[0] || '';
}
},
async sendMessage() {
if (!this.userInput.trim() || this.isLoading) return;
const message = this.userInput;
this.userInput = '';
this.messages.push({ role: 'user', content: message });
this.isLoading = true;
this.scrollToBottom('chatContainer');
try {
const response = await fetch('/api/chat/phase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
phase: this.activePhase,
provider: this.selectedProvider,
model: this.selectedModel,
findings: this.findings
})
});
const data = await response.json();
const aiMessage = {
role: 'assistant',
content: data.content || 'Sorry, I encountered an error.',
phase: this.getCurrentPhase().name,
risk_score: data.risk_score,
findings: data.findings
};
this.messages.push(aiMessage);
if (data.findings && data.findings.length > 0) {
this.findings = [...this.findings, ...data.findings];
}
} catch (e) {
this.messages.push({
role: 'assistant',
content: '❌ Failed to connect to the backend service.',
phase: this.getCurrentPhase().name
});
}
this.isLoading = false;
this.scrollToBottom('chatContainer');
},
async executeCommand() {
if (!this.terminalInput.trim() || this.terminalLoading) return;
const command = this.terminalInput;
this.commandHistory.unshift(command);
this.historyIndex = -1;
this.terminalInput = '';
this.terminalHistory.push({ type: 'cmd', content: command });
this.terminalLoading = true;
this.scrollToBottom('terminalOutput');
try {
const response = await fetch('/api/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: command, timeout: 300 })
});
const data = await response.json();
if (data.stdout) this.terminalHistory.push({ type: 'stdout', content: data.stdout });
if (data.stderr) this.terminalHistory.push({ type: 'stderr', content: data.stderr });
if (data.exit_code !== 0) this.terminalHistory.push({ type: 'info', content: `Exit code: ${data.exit_code}` });
} catch (e) {
this.terminalHistory.push({ type: 'stderr', content: 'Failed to execute command: ' + e.message });
}
this.terminalLoading = false;
this.scrollToBottom('terminalOutput');
},
historyUp() {
if (this.historyIndex < this.commandHistory.length - 1) {
this.historyIndex++;
this.terminalInput = this.commandHistory[this.historyIndex];
}
},
historyDown() {
if (this.historyIndex > 0) {
this.historyIndex--;
this.terminalInput = this.commandHistory[this.historyIndex];
} else if (this.historyIndex === 0) {
this.historyIndex = -1;
this.terminalInput = '';
}
},
executeQuickAction(action) {
if (action.tool) {
this.showScanModal(action.tool, action.scanType);
} else if (action.prompt) {
this.userInput = action.prompt;
this.activeTab = 'phase';
this.sendMessage();
} else if (action.command) {
this.terminalInput = action.command;
this.activeTab = 'terminal';
}
},
showScanModal(tool, scanType) {
const toolConfig = {
nmap: ['quick', 'full', 'stealth', 'vuln'],
nikto: ['default', 'ssl', 'full'],
gobuster: ['dir', 'dns'],
sqlmap: ['test', 'dbs'],
whatweb: ['default', 'aggressive'],
amass: ['default'],
hydra: ['default']
};
this.scanModal = { tool, target: '', scanType, types: toolConfig[tool] || [scanType] };
this.scanModalOpen = true;
},
async startScan() {
if (!this.scanModal.target) return;
try {
const response = await fetch('/api/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tool: this.scanModal.tool,
target: this.scanModal.target,
scan_type: this.scanModal.scanType
})
});
const data = await response.json();
this.scanModalOpen = false;
await this.refreshScans();
this.messages.push({
role: 'assistant',
phase: this.getCurrentPhase().name,
content: `🔄 Started **${this.scanModal.tool}** scan on \`${this.scanModal.target}\`\n\nScan ID: \`${data.scan_id}\``
});
} catch (e) { console.error('Failed to start scan:', e); }
},
async refreshScans() {
try {
const response = await fetch('/api/scans');
this.scans = await response.json();
this.scans.filter(s => s.status === 'completed' && s.parsed).forEach(scan => {
if (scan.parsed.findings) {
scan.parsed.findings.forEach(f => {
if (!this.findings.find(existing => existing.raw === f.raw)) {
this.findings.push({ id: `${scan.scan_id}-${this.findings.length}`, ...f, tool: scan.tool, target: scan.target });
}
});
}
});
} catch (e) { console.error('Failed to load scans:', e); }
},
async pollScan(scanId) {
try {
const response = await fetch(`/api/scan/${scanId}`);
const scan = await response.json();
const index = this.scans.findIndex(s => s.scan_id === scanId);
if (index !== -1) this.scans[index] = scan;
} catch (e) { console.error('Failed to poll scan:', e); }
},
async pollRunningScans() {
for (const scan of this.scans) {
if (scan.status === 'running' || scan.status === 'pending') await this.pollScan(scan.scan_id);
}
},
viewScanDetails(scan) { this.selectedScan = scan; this.detailsModalOpen = true; },
askAboutTool(tool) {
this.activeTab = 'phase';
this.userInput = `Explain how to use ${tool.name} for ${this.getCurrentPhase().name.toLowerCase()}. Include common options and example commands.`;
this.sendMessage();
},
async analyzeAttackChains() {
if (this.findings.length === 0) return;
this.isLoading = true;
try {
const response = await fetch('/api/attack-chains', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ findings: this.findings, provider: this.selectedProvider, model: this.selectedModel })
});
const data = await response.json();
this.attackChains = data.attack_chains || [];
this.activeTab = 'attack-chains';
} catch (e) { console.error('Failed to analyze attack chains:', e); }
this.isLoading = false;
},
countBySeverity(severity) { return this.findings.filter(f => f.severity === severity).length; },
getSeverityBadgeClass(severity) {
const classes = {
critical: 'bg-sev-critical/20 text-sev-critical border border-sev-critical/50',
high: 'bg-sev-high/20 text-sev-high border border-sev-high/50',
medium: 'bg-sev-medium/20 text-sev-medium border border-sev-medium/50',
low: 'bg-sev-low/20 text-sev-low border border-sev-low/50',
info: 'bg-sev-info/20 text-sev-info border border-sev-info/50'
};
return classes[severity] || classes.info;
},
getRiskColor(score) {
if (score >= 9) return 'bg-sev-critical/20 text-sev-critical border border-sev-critical/50';
if (score >= 7) return 'bg-sev-high/20 text-sev-high border border-sev-high/50';
if (score >= 4) return 'bg-sev-medium/20 text-sev-medium border border-sev-medium/50';
return 'bg-sev-low/20 text-sev-low border border-sev-low/50';
},
getRiskLabel(score) {
if (score >= 9) return 'CRITICAL';
if (score >= 7) return 'HIGH';
if (score >= 4) return 'MEDIUM';
return 'LOW';
},
renderMarkdown(content) { return marked.parse(content || ''); },
scrollToBottom(containerId) {
this.$nextTick(() => {
const container = document.getElementById(containerId);
if (container) container.scrollTop = container.scrollHeight;
});
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Expose port
EXPOSE 8001
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
fastapi==0.115.5
uvicorn[standard]==0.32.1
httpx==0.28.1
pydantic==2.10.2

View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Expose port
EXPOSE 8002
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8002"]

View File

@@ -0,0 +1 @@
"""Kali Executor Service"""

View File

@@ -0,0 +1,480 @@
"""
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)

View File

@@ -0,0 +1,5 @@
fastapi==0.115.5
uvicorn[standard]==0.32.1
docker==7.1.0
pydantic==2.10.2
websockets==14.1

58
services/kali/Dockerfile Normal file
View File

@@ -0,0 +1,58 @@
FROM kalilinux/kali-rolling
# Avoid prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Update and install essential security tools
RUN apt-get update && apt-get install -y --no-install-recommends \
# Core utilities
curl \
wget \
git \
vim \
net-tools \
iputils-ping \
dnsutils \
# Reconnaissance tools
nmap \
masscan \
amass \
theharvester \
whatweb \
dnsrecon \
fierce \
# Web testing tools
nikto \
gobuster \
dirb \
sqlmap \
# Network tools
netcat-openbsd \
tcpdump \
wireshark-common \
hydra \
# Exploitation
metasploit-framework \
exploitdb \
# Scripting
python3 \
python3-pip \
python3-venv \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install additional Python tools
RUN pip3 install --break-system-packages \
requests \
beautifulsoup4 \
shodan \
censys
# Create workspace directory
WORKDIR /workspace
# Copy entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,21 @@
#!/bin/bash
echo "=================================================="
echo " StrikePackageGPT - Kali Container"
echo " Security Tools Ready"
echo "=================================================="
echo ""
echo "Available tools:"
echo " - nmap, masscan (port scanning)"
echo " - amass, theharvester (reconnaissance)"
echo " - nikto, gobuster (web testing)"
echo " - sqlmap (SQL injection)"
echo " - hydra (brute force)"
echo " - metasploit (exploitation)"
echo " - searchsploit (exploit database)"
echo ""
echo "Container is ready for security testing."
echo ""
# Keep container running
exec sleep infinity

View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

View File

@@ -0,0 +1,213 @@
"""
LLM Router Service
Routes requests to different LLM providers (OpenAI, Anthropic, Ollama)
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional, Literal
import httpx
import os
app = FastAPI(
title="LLM Router",
description="Routes requests to multiple LLM providers",
version="0.1.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Configuration from environment
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://192.168.1.50:11434")
class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
provider: Literal["openai", "anthropic", "ollama"] = "ollama"
model: str = "llama3.2"
messages: list[ChatMessage]
temperature: float = 0.7
max_tokens: int = 2048
class ChatResponse(BaseModel):
provider: str
model: str
content: str
usage: Optional[dict] = None
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "service": "llm-router"}
@app.get("/providers")
async def list_providers():
"""List available LLM providers and their status"""
# Dynamically fetch Ollama models
ollama_models = []
ollama_available = False
try:
async with httpx.AsyncClient() as client:
response = await client.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5.0)
if response.status_code == 200:
data = response.json()
ollama_models = [m["name"] for m in data.get("models", [])]
ollama_available = True
except Exception:
ollama_models = ["llama3", "mistral", "codellama"] # fallback
providers = {
"openai": {"available": bool(OPENAI_API_KEY), "models": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"]},
"anthropic": {"available": bool(ANTHROPIC_API_KEY), "models": ["claude-sonnet-4-20250514", "claude-3-5-haiku-20241022"]},
"ollama": {"available": ollama_available, "base_url": OLLAMA_BASE_URL, "models": ollama_models}
}
return providers
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""Route chat request to specified LLM provider"""
if request.provider == "openai":
return await _call_openai(request)
elif request.provider == "anthropic":
return await _call_anthropic(request)
elif request.provider == "ollama":
return await _call_ollama(request)
else:
raise HTTPException(status_code=400, detail=f"Unknown provider: {request.provider}")
async def _call_openai(request: ChatRequest) -> ChatResponse:
"""Call OpenAI API"""
if not OPENAI_API_KEY:
raise HTTPException(status_code=503, detail="OpenAI API key not configured")
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.openai.com/v1/chat/completions",
headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": request.model,
"messages": [m.model_dump() for m in request.messages],
"temperature": request.temperature,
"max_tokens": request.max_tokens
},
timeout=60.0
)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
return ChatResponse(
provider="openai",
model=request.model,
content=data["choices"][0]["message"]["content"],
usage=data.get("usage")
)
async def _call_anthropic(request: ChatRequest) -> ChatResponse:
"""Call Anthropic API"""
if not ANTHROPIC_API_KEY:
raise HTTPException(status_code=503, detail="Anthropic API key not configured")
# Extract system message if present
system_msg = ""
messages = []
for msg in request.messages:
if msg.role == "system":
system_msg = msg.content
else:
messages.append({"role": msg.role, "content": msg.content})
async with httpx.AsyncClient() as client:
payload = {
"model": request.model,
"messages": messages,
"max_tokens": request.max_tokens,
"temperature": request.temperature
}
if system_msg:
payload["system"] = system_msg
response = await client.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": ANTHROPIC_API_KEY,
"Content-Type": "application/json",
"anthropic-version": "2023-06-01"
},
json=payload,
timeout=60.0
)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
return ChatResponse(
provider="anthropic",
model=request.model,
content=data["content"][0]["text"],
usage=data.get("usage")
)
async def _call_ollama(request: ChatRequest) -> ChatResponse:
"""Call Ollama API (local models)"""
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{OLLAMA_BASE_URL}/api/chat",
json={
"model": request.model,
"messages": [m.model_dump() for m in request.messages],
"stream": False,
"options": {
"temperature": request.temperature,
"num_predict": request.max_tokens
}
},
timeout=120.0
)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
return ChatResponse(
provider="ollama",
model=request.model,
content=data["message"]["content"],
usage={
"prompt_tokens": data.get("prompt_eval_count", 0),
"completion_tokens": data.get("eval_count", 0)
}
)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Ollama service not available")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -0,0 +1,4 @@
fastapi==0.115.5
uvicorn[standard]==0.32.1
httpx==0.28.1
pydantic==2.10.2

View File

@@ -0,0 +1,4 @@
"""
StrikePackageGPT Shared Library
Common models, utilities, and constants used across services.
"""

158
services/shared/models.py Normal file
View File

@@ -0,0 +1,158 @@
"""
Shared Pydantic models for StrikePackageGPT services.
"""
from pydantic import BaseModel, Field
from typing import Optional, Literal, List, Dict, Any
from datetime import datetime
from enum import Enum
class TaskState(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class ToolCategory(str, Enum):
RECON = "reconnaissance"
VULN_SCAN = "vulnerability_scanning"
EXPLOITATION = "exploitation"
WEB_TESTING = "web_testing"
PASSWORD = "password_attacks"
WIRELESS = "wireless"
FORENSICS = "forensics"
# ============== Chat Models ==============
class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
timestamp: Optional[datetime] = None
class ChatRequest(BaseModel):
message: str
session_id: Optional[str] = None
context: Optional[str] = None
provider: str = "ollama"
model: str = "llama3.2"
temperature: float = 0.7
max_tokens: int = 2048
class ChatResponse(BaseModel):
provider: str
model: str
content: str
usage: Optional[Dict[str, int]] = None
session_id: Optional[str] = None
# ============== Command Execution Models ==============
class CommandRequest(BaseModel):
command: str
timeout: int = Field(default=300, ge=1, le=3600)
working_dir: Optional[str] = "/workspace"
env: Optional[Dict[str, str]] = None
class CommandResult(BaseModel):
command: str
exit_code: int
stdout: str
stderr: str
duration_seconds: float
timed_out: bool = False
# ============== Task Models ==============
class Task(BaseModel):
task_id: str
task_type: str
status: TaskState = TaskState.PENDING
created_at: datetime = Field(default_factory=datetime.utcnow)
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
result: Optional[Any] = None
error: Optional[str] = None
progress: int = Field(default=0, ge=0, le=100)
metadata: Dict[str, Any] = Field(default_factory=dict)
# ============== Security Tool Models ==============
class SecurityTool(BaseModel):
name: str
description: str
category: ToolCategory
command_template: str
required_args: List[str] = []
optional_args: List[str] = []
output_parser: Optional[str] = None
class ScanTarget(BaseModel):
target: str # IP, hostname, URL, or CIDR
target_type: Literal["ip", "hostname", "url", "cidr", "auto"] = "auto"
ports: Optional[str] = None # e.g., "22,80,443" or "1-1000"
options: Dict[str, Any] = Field(default_factory=dict)
class ScanRequest(BaseModel):
target: ScanTarget
tool: str
scan_type: Optional[str] = None
options: Dict[str, Any] = Field(default_factory=dict)
class ScanResult(BaseModel):
scan_id: str
tool: str
target: str
status: TaskState
started_at: datetime
completed_at: Optional[datetime] = None
raw_output: Optional[str] = None
parsed_results: Optional[Dict[str, Any]] = None
findings: List[Dict[str, Any]] = []
# ============== Session Models ==============
class Session(BaseModel):
session_id: str
created_at: datetime = Field(default_factory=datetime.utcnow)
last_activity: datetime = Field(default_factory=datetime.utcnow)
messages: List[ChatMessage] = []
context: Dict[str, Any] = Field(default_factory=dict)
active_scans: List[str] = []
# ============== Finding Models ==============
class Severity(str, Enum):
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
INFO = "info"
class Finding(BaseModel):
finding_id: str
title: str
description: str
severity: Severity
category: str
target: str
evidence: Optional[str] = None
remediation: Optional[str] = None
references: List[str] = []
cve_ids: List[str] = []
detected_at: datetime = Field(default_factory=datetime.utcnow)
tool: Optional[str] = None

315
services/shared/parsers.py Normal file
View File

@@ -0,0 +1,315 @@
"""
Output parsers for security tool results.
Converts raw tool output into structured data.
"""
import re
import json
import xml.etree.ElementTree as ET
from typing import Dict, Any, List, Optional
from datetime import datetime
class BaseParser:
"""Base class for tool output parsers."""
def parse(self, output: str) -> Dict[str, Any]:
raise NotImplementedError
class NmapParser(BaseParser):
"""Parser for nmap output."""
def parse(self, output: str) -> Dict[str, Any]:
"""Parse nmap text output."""
results = {
"hosts": [],
"scan_info": {},
"raw": output
}
current_host = None
for line in output.split('\n'):
line = line.strip()
# Parse scan info
if line.startswith('Nmap scan report for'):
if current_host:
results["hosts"].append(current_host)
# Extract hostname and IP
match = re.search(r'for (\S+)(?: \((\d+\.\d+\.\d+\.\d+)\))?', line)
if match:
current_host = {
"hostname": match.group(1),
"ip": match.group(2) or match.group(1),
"ports": [],
"os": None,
"status": "up"
}
# Parse port info
elif current_host and re.match(r'^\d+/(tcp|udp)', line):
parts = line.split()
if len(parts) >= 3:
port_proto = parts[0].split('/')
current_host["ports"].append({
"port": int(port_proto[0]),
"protocol": port_proto[1],
"state": parts[1],
"service": parts[2] if len(parts) > 2 else "unknown",
"version": ' '.join(parts[3:]) if len(parts) > 3 else None
})
# Parse OS detection
elif current_host and 'OS details:' in line:
current_host["os"] = line.replace('OS details:', '').strip()
# Parse timing info
elif 'scanned in' in line.lower():
match = re.search(r'scanned in ([\d.]+) seconds', line)
if match:
results["scan_info"]["duration_seconds"] = float(match.group(1))
if current_host:
results["hosts"].append(current_host)
return results
def parse_xml(self, xml_output: str) -> Dict[str, Any]:
"""Parse nmap XML output for more detailed results."""
try:
root = ET.fromstring(xml_output)
results = {
"hosts": [],
"scan_info": {
"scanner": root.get("scanner"),
"args": root.get("args"),
"start_time": root.get("start"),
}
}
for host in root.findall('.//host'):
host_info = {
"ip": None,
"hostname": None,
"status": host.find('status').get('state') if host.find('status') is not None else "unknown",
"ports": [],
"os": []
}
# Get addresses
for addr in host.findall('.//address'):
if addr.get('addrtype') == 'ipv4':
host_info["ip"] = addr.get('addr')
# Get hostnames
hostname_elem = host.find('.//hostname')
if hostname_elem is not None:
host_info["hostname"] = hostname_elem.get('name')
# Get ports
for port in host.findall('.//port'):
port_info = {
"port": int(port.get('portid')),
"protocol": port.get('protocol'),
"state": port.find('state').get('state') if port.find('state') is not None else "unknown",
}
service = port.find('service')
if service is not None:
port_info["service"] = service.get('name')
port_info["product"] = service.get('product')
port_info["version"] = service.get('version')
host_info["ports"].append(port_info)
results["hosts"].append(host_info)
return results
except ET.ParseError:
return {"error": "Failed to parse XML", "raw": xml_output}
class NiktoParser(BaseParser):
"""Parser for nikto output."""
def parse(self, output: str) -> Dict[str, Any]:
results = {
"target": None,
"findings": [],
"server_info": {},
"raw": output
}
for line in output.split('\n'):
line = line.strip()
# Target info
if '+ Target IP:' in line:
results["target"] = line.split(':')[-1].strip()
elif '+ Target Hostname:' in line:
results["server_info"]["hostname"] = line.split(':')[-1].strip()
elif '+ Target Port:' in line:
results["server_info"]["port"] = line.split(':')[-1].strip()
elif '+ Server:' in line:
results["server_info"]["server"] = line.split(':', 1)[-1].strip()
# Findings (lines starting with +)
elif line.startswith('+') and ':' in line:
# Skip info lines
if any(skip in line for skip in ['Target IP', 'Target Hostname', 'Target Port', 'Server:', 'Start Time', 'End Time']):
continue
finding = {
"raw": line[1:].strip(),
"severity": "info"
}
# Determine severity based on content
if any(word in line.lower() for word in ['vulnerable', 'vulnerability', 'exploit']):
finding["severity"] = "high"
elif any(word in line.lower() for word in ['outdated', 'deprecated', 'insecure']):
finding["severity"] = "medium"
elif any(word in line.lower() for word in ['disclosed', 'information', 'header']):
finding["severity"] = "low"
# Extract OSVDB if present
osvdb_match = re.search(r'OSVDB-(\d+)', line)
if osvdb_match:
finding["osvdb"] = osvdb_match.group(1)
results["findings"].append(finding)
return results
class SQLMapParser(BaseParser):
"""Parser for sqlmap output."""
def parse(self, output: str) -> Dict[str, Any]:
results = {
"target": None,
"parameters": [],
"injections": [],
"databases": [],
"raw": output
}
in_parameter_section = False
for line in output.split('\n'):
line = line.strip()
# Target URL
if 'target URL' in line.lower():
match = re.search(r"'([^']+)'", line)
if match:
results["target"] = match.group(1)
# Injectable parameters
if 'Parameter:' in line:
param_match = re.search(r"Parameter: (\S+)", line)
if param_match:
results["parameters"].append({
"name": param_match.group(1),
"injectable": True
})
# Injection type
if 'Type:' in line and 'injection' in line.lower():
results["injections"].append(line.replace('Type:', '').strip())
# Databases found
if line.startswith('[*]') and 'available databases' not in line.lower():
db_name = line[3:].strip()
if db_name:
results["databases"].append(db_name)
return results
class GobusterParser(BaseParser):
"""Parser for gobuster output."""
def parse(self, output: str) -> Dict[str, Any]:
results = {
"findings": [],
"directories": [],
"files": [],
"raw": output
}
for line in output.split('\n'):
line = line.strip()
# Parse found paths
# Format: /path (Status: 200) [Size: 1234]
match = re.search(r'^(/\S*)\s+\(Status:\s*(\d+)\)(?:\s+\[Size:\s*(\d+)\])?', line)
if match:
finding = {
"path": match.group(1),
"status": int(match.group(2)),
"size": int(match.group(3)) if match.group(3) else None
}
results["findings"].append(finding)
if finding["path"].endswith('/'):
results["directories"].append(finding["path"])
else:
results["files"].append(finding["path"])
return results
class HydraParser(BaseParser):
"""Parser for hydra output."""
def parse(self, output: str) -> Dict[str, Any]:
results = {
"credentials": [],
"target": None,
"service": None,
"raw": output
}
for line in output.split('\n'):
line = line.strip()
# Parse found credentials
# Format: [port][service] host: x login: y password: z
cred_match = re.search(r'\[(\d+)\]\[(\w+)\]\s+host:\s+(\S+)\s+login:\s+(\S+)\s+password:\s+(\S+)', line)
if cred_match:
results["credentials"].append({
"port": int(cred_match.group(1)),
"service": cred_match.group(2),
"host": cred_match.group(3),
"username": cred_match.group(4),
"password": cred_match.group(5)
})
results["target"] = cred_match.group(3)
results["service"] = cred_match.group(2)
return results
# Registry of parsers
PARSERS = {
"nmap": NmapParser(),
"nikto": NiktoParser(),
"sqlmap": SQLMapParser(),
"gobuster": GobusterParser(),
"hydra": HydraParser(),
}
def parse_tool_output(tool: str, output: str) -> Dict[str, Any]:
"""Parse output from a security tool."""
parser = PARSERS.get(tool.lower())
if parser:
try:
return parser.parse(output)
except Exception as e:
return {"error": str(e), "raw": output}
return {"raw": output}

263
services/shared/tools.py Normal file
View File

@@ -0,0 +1,263 @@
"""
Security tool definitions and command builders.
"""
from typing import Dict, List, Optional, Any
SECURITY_TOOLS = {
# ============== Reconnaissance ==============
"nmap": {
"name": "nmap",
"description": "Network scanner and security auditing tool",
"category": "reconnaissance",
"templates": {
"quick": "nmap -T4 -F {target}",
"full": "nmap -sV -sC -O -p- {target}",
"stealth": "nmap -sS -T2 -f {target}",
"udp": "nmap -sU --top-ports 100 {target}",
"vuln": "nmap --script vuln {target}",
"version": "nmap -sV -p {ports} {target}",
"os": "nmap -O --osscan-guess {target}",
},
"default_template": "quick",
"output_parser": "nmap"
},
"masscan": {
"name": "masscan",
"description": "Fast TCP port scanner",
"category": "reconnaissance",
"templates": {
"quick": "masscan {target} --ports 0-1000 --rate 1000",
"full": "masscan {target} --ports 0-65535 --rate 10000",
"top100": "masscan {target} --top-ports 100 --rate 1000",
},
"default_template": "quick",
},
"amass": {
"name": "amass",
"description": "Subdomain enumeration tool",
"category": "reconnaissance",
"templates": {
"passive": "amass enum -passive -d {target}",
"active": "amass enum -active -d {target}",
"intel": "amass intel -d {target}",
},
"default_template": "passive",
},
"theharvester": {
"name": "theHarvester",
"description": "OSINT tool for gathering emails, names, subdomains",
"category": "reconnaissance",
"templates": {
"all": "theHarvester -d {target} -b all",
"google": "theHarvester -d {target} -b google",
"linkedin": "theHarvester -d {target} -b linkedin",
},
"default_template": "all",
},
"whatweb": {
"name": "whatweb",
"description": "Web technology fingerprinting",
"category": "reconnaissance",
"templates": {
"default": "whatweb {target}",
"aggressive": "whatweb -a 3 {target}",
"verbose": "whatweb -v {target}",
},
"default_template": "default",
},
"dnsrecon": {
"name": "dnsrecon",
"description": "DNS enumeration tool",
"category": "reconnaissance",
"templates": {
"standard": "dnsrecon -d {target}",
"zone": "dnsrecon -d {target} -t axfr",
"brute": "dnsrecon -d {target} -t brt",
},
"default_template": "standard",
},
# ============== Vulnerability Scanning ==============
"nikto": {
"name": "nikto",
"description": "Web server vulnerability scanner",
"category": "vulnerability_scanning",
"templates": {
"default": "nikto -h {target}",
"ssl": "nikto -h {target} -ssl",
"tuning": "nikto -h {target} -Tuning x",
"full": "nikto -h {target} -C all",
},
"default_template": "default",
"output_parser": "nikto"
},
"sqlmap": {
"name": "sqlmap",
"description": "SQL injection detection and exploitation",
"category": "vulnerability_scanning",
"templates": {
"test": "sqlmap -u '{target}' --batch",
"dbs": "sqlmap -u '{target}' --batch --dbs",
"tables": "sqlmap -u '{target}' --batch -D {database} --tables",
"dump": "sqlmap -u '{target}' --batch -D {database} -T {table} --dump",
"forms": "sqlmap -u '{target}' --batch --forms",
},
"default_template": "test",
"output_parser": "sqlmap"
},
"wpscan": {
"name": "wpscan",
"description": "WordPress vulnerability scanner",
"category": "vulnerability_scanning",
"templates": {
"default": "wpscan --url {target}",
"enumerate": "wpscan --url {target} -e vp,vt,u",
"aggressive": "wpscan --url {target} -e ap,at,u --plugins-detection aggressive",
},
"default_template": "default",
},
# ============== Web Testing ==============
"gobuster": {
"name": "gobuster",
"description": "Directory/file brute-forcing",
"category": "web_testing",
"templates": {
"dir": "gobuster dir -u {target} -w /usr/share/wordlists/dirb/common.txt",
"big": "gobuster dir -u {target} -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt",
"dns": "gobuster dns -d {target} -w /usr/share/wordlists/dns/subdomains-top1million-5000.txt",
"vhost": "gobuster vhost -u {target} -w /usr/share/wordlists/dns/subdomains-top1million-5000.txt",
},
"default_template": "dir",
"output_parser": "gobuster"
},
"ffuf": {
"name": "ffuf",
"description": "Fast web fuzzer",
"category": "web_testing",
"templates": {
"dir": "ffuf -u {target}/FUZZ -w /usr/share/wordlists/dirb/common.txt",
"vhost": "ffuf -u {target} -H 'Host: FUZZ.{domain}' -w /usr/share/wordlists/dns/subdomains-top1million-5000.txt",
"param": "ffuf -u '{target}?FUZZ=test' -w /usr/share/wordlists/dirb/common.txt",
},
"default_template": "dir",
},
"dirb": {
"name": "dirb",
"description": "Web content scanner",
"category": "web_testing",
"templates": {
"default": "dirb {target}",
"small": "dirb {target} /usr/share/wordlists/dirb/small.txt",
"big": "dirb {target} /usr/share/wordlists/dirb/big.txt",
},
"default_template": "default",
},
# ============== Exploitation ==============
"searchsploit": {
"name": "searchsploit",
"description": "Exploit database search tool",
"category": "exploitation",
"templates": {
"search": "searchsploit {query}",
"exact": "searchsploit -e {query}",
"json": "searchsploit -j {query}",
"path": "searchsploit -p {exploit_id}",
},
"default_template": "search",
},
"hydra": {
"name": "hydra",
"description": "Network login cracker",
"category": "password_attacks",
"templates": {
"ssh": "hydra -l {user} -P /usr/share/wordlists/rockyou.txt {target} ssh",
"ftp": "hydra -l {user} -P /usr/share/wordlists/rockyou.txt {target} ftp",
"http_post": "hydra -l {user} -P /usr/share/wordlists/rockyou.txt {target} http-post-form '{form}'",
"smb": "hydra -l {user} -P /usr/share/wordlists/rockyou.txt {target} smb",
},
"default_template": "ssh",
"output_parser": "hydra"
},
# ============== Network Tools ==============
"netcat": {
"name": "nc",
"description": "Network utility for TCP/UDP connections",
"category": "network",
"templates": {
"listen": "nc -lvnp {port}",
"connect": "nc -v {target} {port}",
"scan": "nc -zv {target} {port_range}",
},
"default_template": "scan",
},
"curl": {
"name": "curl",
"description": "HTTP client",
"category": "web_testing",
"templates": {
"get": "curl -v {target}",
"headers": "curl -I {target}",
"post": "curl -X POST -d '{data}' {target}",
"follow": "curl -L -v {target}",
},
"default_template": "get",
},
}
def get_tool(name: str) -> Optional[Dict[str, Any]]:
"""Get tool definition by name."""
return SECURITY_TOOLS.get(name.lower())
def get_tools_by_category(category: str) -> List[Dict[str, Any]]:
"""Get all tools in a category."""
return [tool for tool in SECURITY_TOOLS.values() if tool.get("category") == category]
def build_command(tool_name: str, template_name: str = None, **kwargs) -> Optional[str]:
"""Build a command from a tool template."""
tool = get_tool(tool_name)
if not tool:
return None
template_name = template_name or tool.get("default_template")
template = tool.get("templates", {}).get(template_name)
if not template:
return None
try:
return template.format(**kwargs)
except KeyError as e:
return None
def list_all_tools() -> Dict[str, List[Dict[str, str]]]:
"""List all available tools grouped by category."""
result = {}
for tool in SECURITY_TOOLS.values():
category = tool.get("category", "other")
if category not in result:
result[category] = []
result[category].append({
"name": tool["name"],
"description": tool["description"],
"templates": list(tool.get("templates", {}).keys())
})
return result