mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
Add Vite React component bundling, SSE process streaming, preferences persistence, WebSocket terminal proxy, local Ollama integration
- Enable local Ollama service in compose with llm-router dependency - Add SSE /stream/processes endpoint in kali-executor for live process updates - Add WebSocket /ws/execute for real-time terminal command streaming - Implement preferences persistence (provider/model) via dashboard backend - Create Vite build pipeline for React components (VoiceControls, NetworkMap, GuidedWizard) - Update dashboard Dockerfile with Node builder stage for component bundling - Wire dashboard template to mount components and subscribe to SSE/WebSocket streams - Add preferences load/save hooks in UI to persist LLM provider/model selection
This commit is contained in:
@@ -67,13 +67,13 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
# Local Ollama on host machine (use host.docker.internal on Windows/Mac)
|
# Prefer local Ollama container for self-contained setup
|
||||||
- OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://host.docker.internal:11434}
|
- OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://strikepackage-ollama:11434}
|
||||||
# Network Ollama instances (Dell LLM box with larger models)
|
# Network Ollama instances (Dell LLM box with larger models)
|
||||||
- OLLAMA_NETWORK_URLS=${OLLAMA_NETWORK_URLS:-http://192.168.1.50:11434}
|
- OLLAMA_NETWORK_URLS=${OLLAMA_NETWORK_URLS:-http://192.168.1.50:11434}
|
||||||
# Legacy single endpoint (fallback)
|
# Legacy single endpoint (fallback)
|
||||||
- OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://host.docker.internal:11434}
|
- OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://strikepackage-ollama:11434}
|
||||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://strikepackage-ollama:11434}
|
||||||
# Load balancing: round-robin, random, failover
|
# Load balancing: round-robin, random, failover
|
||||||
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-failover}
|
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-failover}
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
@@ -81,6 +81,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- strikepackage-net
|
- strikepackage-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- ollama
|
||||||
|
|
||||||
# Kali Linux - Security tools container
|
# Kali Linux - Security tools container
|
||||||
kali:
|
kali:
|
||||||
@@ -100,26 +102,25 @@ services:
|
|||||||
- NET_RAW
|
- NET_RAW
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Ollama - Local LLM (disabled - using Dell LLM box at 192.168.1.50)
|
# Ollama - Local LLM
|
||||||
# Uncomment to use local Ollama instead
|
ollama:
|
||||||
# ollama:
|
image: ollama/ollama:latest
|
||||||
# image: ollama/ollama:latest
|
container_name: strikepackage-ollama
|
||||||
# container_name: strikepackage-ollama
|
ports:
|
||||||
# ports:
|
- "11434:11434"
|
||||||
# - "11434:11434"
|
volumes:
|
||||||
# volumes:
|
- ollama-models:/root/.ollama
|
||||||
# - ollama-models:/root/.ollama
|
networks:
|
||||||
# networks:
|
- strikepackage-net
|
||||||
# - strikepackage-net
|
restart: unless-stopped
|
||||||
# restart: unless-stopped
|
# GPU support (optional): uncomment if using NVIDIA GPU
|
||||||
# # Uncomment for GPU support:
|
# deploy:
|
||||||
# # deploy:
|
# resources:
|
||||||
# # resources:
|
# reservations:
|
||||||
# # reservations:
|
# devices:
|
||||||
# # devices:
|
# - driver: nvidia
|
||||||
# # - driver: nvidia
|
# count: all
|
||||||
# # count: all
|
# capabilities: [gpu]
|
||||||
# # capabilities: [gpu]
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
strikepackage-net:
|
strikepackage-net:
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
FROM node:20-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy package files and JSX components
|
||||||
|
COPY package.json vite.config.js ./
|
||||||
|
COPY components/ ./components/
|
||||||
|
|
||||||
|
# Install dependencies and build
|
||||||
|
RUN npm install && npm run build
|
||||||
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -11,6 +22,9 @@ COPY app/ ./app/
|
|||||||
COPY templates/ ./templates/
|
COPY templates/ ./templates/
|
||||||
COPY static/ ./static/
|
COPY static/ ./static/
|
||||||
|
|
||||||
|
# Copy built components from builder stage
|
||||||
|
COPY --from=builder /build/static/dist/ ./static/dist/
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
@@ -432,7 +432,15 @@ async def health_check():
|
|||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
"""Main dashboard page"""
|
"""Main dashboard page"""
|
||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"HACKGPT_API_URL": HACKGPT_API_URL,
|
||||||
|
"LLM_ROUTER_URL": LLM_ROUTER_URL,
|
||||||
|
"KALI_EXECUTOR_URL": KALI_EXECUTOR_URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/terminal", response_class=HTMLResponse)
|
@app.get("/terminal", response_class=HTMLResponse)
|
||||||
@@ -475,6 +483,23 @@ async def get_running_processes():
|
|||||||
except:
|
except:
|
||||||
return {"running_processes": [], "count": 0}
|
return {"running_processes": [], "count": 0}
|
||||||
|
|
||||||
|
@app.get("/api/stream/processes")
|
||||||
|
async def stream_running_processes():
|
||||||
|
"""Server-Sent Events stream proxy that emits running processes periodically."""
|
||||||
|
|
||||||
|
async def event_generator():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(f"{KALI_EXECUTOR_URL}/processes", timeout=10.0)
|
||||||
|
data = response.json() if response.status_code == 200 else {"running_processes": [], "count": 0}
|
||||||
|
yield f"data: {json.dumps(data)}\n\n"
|
||||||
|
except Exception as e:
|
||||||
|
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/providers")
|
@app.get("/api/providers")
|
||||||
async def get_providers():
|
async def get_providers():
|
||||||
@@ -501,6 +526,44 @@ async def get_tools():
|
|||||||
except httpx.ConnectError:
|
except httpx.ConnectError:
|
||||||
raise HTTPException(status_code=503, detail="HackGPT API not available")
|
raise HTTPException(status_code=503, detail="HackGPT API not available")
|
||||||
|
|
||||||
|
# ============= PREFERENCES ENDPOINTS =============
|
||||||
|
|
||||||
|
PREFERENCES_PATH = Path("/app/data/preferences.json")
|
||||||
|
|
||||||
|
def load_preferences() -> Dict[str, Any]:
|
||||||
|
if PREFERENCES_PATH.exists():
|
||||||
|
try:
|
||||||
|
with open(PREFERENCES_PATH, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_preferences(prefs: Dict[str, Any]):
|
||||||
|
PREFERENCES_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(PREFERENCES_PATH, "w") as f:
|
||||||
|
json.dump(prefs, f, indent=2)
|
||||||
|
|
||||||
|
class Preferences(BaseModel):
|
||||||
|
provider: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
project_id: Optional[str] = None
|
||||||
|
|
||||||
|
@app.get("/api/preferences")
|
||||||
|
async def get_preferences(project_id: Optional[str] = None):
|
||||||
|
prefs = load_preferences()
|
||||||
|
if project_id and project_id in prefs:
|
||||||
|
return prefs[project_id]
|
||||||
|
return prefs.get("global", {})
|
||||||
|
|
||||||
|
@app.post("/api/preferences")
|
||||||
|
async def set_preferences(preferences: Preferences):
|
||||||
|
prefs = load_preferences()
|
||||||
|
key = preferences.project_id or "global"
|
||||||
|
prefs[key] = {"provider": preferences.provider, "model": preferences.model}
|
||||||
|
save_preferences(prefs)
|
||||||
|
return {"status": "saved"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/chat")
|
@app.post("/api/chat")
|
||||||
async def chat(message: ChatMessage):
|
async def chat(message: ChatMessage):
|
||||||
@@ -2077,6 +2140,513 @@ I can assist with:
|
|||||||
What would you like to know?"""
|
What would you like to know?"""
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# CTF AI Agent
|
||||||
|
# ===========================================
|
||||||
|
class CTFAgentRequest(BaseModel):
|
||||||
|
message: str
|
||||||
|
category: str = "general" # web, crypto, forensics, pwn, reversing, misc, general
|
||||||
|
context: Optional[str] = None # Additional context like challenge description
|
||||||
|
hints_used: int = 0
|
||||||
|
provider: str = "ollama"
|
||||||
|
model: str = "llama3.2"
|
||||||
|
|
||||||
|
|
||||||
|
class CTFHintRequest(BaseModel):
|
||||||
|
challenge_name: str
|
||||||
|
challenge_description: str
|
||||||
|
category: str
|
||||||
|
what_tried: Optional[str] = None
|
||||||
|
hint_level: int = 1 # 1=subtle, 2=moderate, 3=direct
|
||||||
|
|
||||||
|
|
||||||
|
# CTF conversation history per session
|
||||||
|
ctf_sessions: Dict[str, List[Dict]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/ctf/agent")
|
||||||
|
async def ctf_agent_chat(request: CTFAgentRequest):
|
||||||
|
"""AI agent specialized for CTF challenges"""
|
||||||
|
|
||||||
|
# Build specialized CTF prompt based on category
|
||||||
|
system_prompt = get_ctf_system_prompt(request.category)
|
||||||
|
|
||||||
|
# Build the full prompt
|
||||||
|
full_prompt = f"""{system_prompt}
|
||||||
|
|
||||||
|
User's question/request:
|
||||||
|
{request.message}
|
||||||
|
|
||||||
|
{"Additional context: " + request.context if request.context else ""}
|
||||||
|
|
||||||
|
Provide helpful, educational guidance. Focus on teaching the methodology rather than giving direct answers.
|
||||||
|
If they seem stuck, offer progressive hints. Use markdown formatting."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{HACKGPT_API_URL}/chat",
|
||||||
|
json={
|
||||||
|
"message": full_prompt,
|
||||||
|
"provider": request.provider,
|
||||||
|
"model": request.model,
|
||||||
|
"context": "ctf_agent"
|
||||||
|
},
|
||||||
|
timeout=90.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
ai_response = data.get("response", "")
|
||||||
|
|
||||||
|
# Extract any tool suggestions from response
|
||||||
|
tools = extract_ctf_tools(ai_response, request.category)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"response": ai_response,
|
||||||
|
"category": request.category,
|
||||||
|
"suggested_tools": tools,
|
||||||
|
"follow_up_questions": get_follow_up_questions(request.category)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"response": get_ctf_fallback(request.category, request.message), "category": request.category}
|
||||||
|
except Exception as e:
|
||||||
|
return {"response": get_ctf_fallback(request.category, request.message), "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/ctf/hint")
|
||||||
|
async def get_ctf_hint(request: CTFHintRequest):
|
||||||
|
"""Get progressive hints for a CTF challenge"""
|
||||||
|
|
||||||
|
hint_prompts = {
|
||||||
|
1: "Give a very subtle hint that points them in the right direction without revealing the solution. Be cryptic but helpful.",
|
||||||
|
2: "Give a moderate hint that identifies the general technique or vulnerability type they should look for.",
|
||||||
|
3: "Give a direct hint that explains the specific approach needed, but still let them figure out the exact implementation."
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt = f"""You are a CTF mentor helping with a {request.category} challenge.
|
||||||
|
|
||||||
|
Challenge: {request.challenge_name}
|
||||||
|
Description: {request.challenge_description}
|
||||||
|
{"What they've tried: " + request.what_tried if request.what_tried else ""}
|
||||||
|
|
||||||
|
{hint_prompts.get(request.hint_level, hint_prompts[2])}
|
||||||
|
|
||||||
|
Format your hint as:
|
||||||
|
💡 **Hint Level {request.hint_level}**
|
||||||
|
[Your hint here]
|
||||||
|
|
||||||
|
{"🔧 **Suggested Tool/Command**" if request.hint_level >= 2 else ""}"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{HACKGPT_API_URL}/chat",
|
||||||
|
json={
|
||||||
|
"message": prompt,
|
||||||
|
"provider": "ollama",
|
||||||
|
"model": "llama3.2",
|
||||||
|
"context": "ctf_hint"
|
||||||
|
},
|
||||||
|
timeout=60.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return {
|
||||||
|
"hint": data.get("response", ""),
|
||||||
|
"level": request.hint_level,
|
||||||
|
"next_level_available": request.hint_level < 3
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback hints
|
||||||
|
return {
|
||||||
|
"hint": get_fallback_hint(request.category, request.hint_level),
|
||||||
|
"level": request.hint_level,
|
||||||
|
"next_level_available": request.hint_level < 3
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/ctf/analyze")
|
||||||
|
async def analyze_ctf_data(data: Dict[str, Any]):
|
||||||
|
"""Analyze CTF data (encoded strings, hashes, files, etc.)"""
|
||||||
|
|
||||||
|
input_data = data.get("input", "")
|
||||||
|
analysis_type = data.get("type", "auto") # auto, encoding, hash, cipher, binary
|
||||||
|
|
||||||
|
results = {
|
||||||
|
"input": input_data[:500], # Truncate for display
|
||||||
|
"detections": [],
|
||||||
|
"suggestions": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect data type
|
||||||
|
if analysis_type == "auto" or analysis_type == "encoding":
|
||||||
|
# Check for Base64
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
|
||||||
|
if re.match(r'^[A-Za-z0-9+/]+=*$', input_data) and len(input_data) % 4 == 0:
|
||||||
|
try:
|
||||||
|
decoded = base64.b64decode(input_data).decode('utf-8', errors='ignore')
|
||||||
|
if decoded.isprintable() or len([c for c in decoded if c.isprintable()]) > len(decoded) * 0.7:
|
||||||
|
results["detections"].append({
|
||||||
|
"type": "Base64",
|
||||||
|
"decoded": decoded[:500],
|
||||||
|
"confidence": "high"
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for Hex
|
||||||
|
if re.match(r'^[0-9a-fA-F]+$', input_data) and len(input_data) % 2 == 0:
|
||||||
|
try:
|
||||||
|
decoded = bytes.fromhex(input_data).decode('utf-8', errors='ignore')
|
||||||
|
results["detections"].append({
|
||||||
|
"type": "Hex",
|
||||||
|
"decoded": decoded[:500],
|
||||||
|
"confidence": "high"
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for ROT13
|
||||||
|
import codecs
|
||||||
|
rot13 = codecs.decode(input_data, 'rot_13')
|
||||||
|
if rot13 != input_data:
|
||||||
|
results["detections"].append({
|
||||||
|
"type": "ROT13 (possible)",
|
||||||
|
"decoded": rot13[:500],
|
||||||
|
"confidence": "medium"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check for hash patterns
|
||||||
|
if analysis_type == "auto" or analysis_type == "hash":
|
||||||
|
hash_patterns = {
|
||||||
|
r'^[a-f0-9]{32}$': "MD5",
|
||||||
|
r'^[a-f0-9]{40}$': "SHA1",
|
||||||
|
r'^[a-f0-9]{64}$': "SHA256",
|
||||||
|
r'^[a-f0-9]{128}$': "SHA512",
|
||||||
|
r'^\$2[ayb]\$.{56}$': "bcrypt",
|
||||||
|
r'^[a-f0-9]{32}:[a-f0-9]+$': "MD5 with salt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for pattern, hash_type in hash_patterns.items():
|
||||||
|
if re.match(pattern, input_data.lower()):
|
||||||
|
results["detections"].append({
|
||||||
|
"type": f"Hash: {hash_type}",
|
||||||
|
"suggestion": f"Try cracking with hashcat or john. Mode for {hash_type}",
|
||||||
|
"confidence": "high"
|
||||||
|
})
|
||||||
|
results["suggestions"].append(f"hashcat -m <mode> '{input_data}' wordlist.txt")
|
||||||
|
|
||||||
|
# Suggest tools based on input
|
||||||
|
if not results["detections"]:
|
||||||
|
results["suggestions"] = [
|
||||||
|
"Try CyberChef for multi-layer encoding",
|
||||||
|
"Check for custom/proprietary encoding",
|
||||||
|
"Look for patterns or repeated sequences",
|
||||||
|
"Try frequency analysis if it might be a cipher"
|
||||||
|
]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/ctf/tools/{category}")
|
||||||
|
async def get_ctf_tools(category: str):
|
||||||
|
"""Get recommended tools for a CTF category"""
|
||||||
|
|
||||||
|
tools = {
|
||||||
|
"web": [
|
||||||
|
{"name": "Burp Suite", "command": "burpsuite", "description": "Web proxy and scanner"},
|
||||||
|
{"name": "SQLMap", "command": "sqlmap -u 'URL' --dbs", "description": "SQL injection automation"},
|
||||||
|
{"name": "Nikto", "command": "nikto -h URL", "description": "Web vulnerability scanner"},
|
||||||
|
{"name": "Gobuster", "command": "gobuster dir -u URL -w wordlist", "description": "Directory brute-force"},
|
||||||
|
{"name": "WFuzz", "command": "wfuzz -c -w wordlist -u URL/FUZZ", "description": "Web fuzzer"},
|
||||||
|
{"name": "XXEinjector", "command": "xxeinjector", "description": "XXE injection tool"},
|
||||||
|
],
|
||||||
|
"crypto": [
|
||||||
|
{"name": "CyberChef", "command": "https://gchq.github.io/CyberChef/", "description": "Encoding/decoding swiss army knife"},
|
||||||
|
{"name": "John the Ripper", "command": "john --wordlist=rockyou.txt hash.txt", "description": "Password cracker"},
|
||||||
|
{"name": "Hashcat", "command": "hashcat -m 0 hash.txt wordlist.txt", "description": "GPU password cracker"},
|
||||||
|
{"name": "RsaCtfTool", "command": "rsactftool -n N -e E --uncipher C", "description": "RSA attack tool"},
|
||||||
|
{"name": "xortool", "command": "xortool -l LENGTH file", "description": "XOR analysis"},
|
||||||
|
],
|
||||||
|
"forensics": [
|
||||||
|
{"name": "Volatility", "command": "volatility -f dump.raw imageinfo", "description": "Memory forensics"},
|
||||||
|
{"name": "Binwalk", "command": "binwalk -e file", "description": "Firmware/file extraction"},
|
||||||
|
{"name": "Foremost", "command": "foremost -i image -o output", "description": "File carving"},
|
||||||
|
{"name": "Exiftool", "command": "exiftool file", "description": "Metadata extraction"},
|
||||||
|
{"name": "Steghide", "command": "steghide extract -sf image.jpg", "description": "Steganography"},
|
||||||
|
{"name": "Strings", "command": "strings file | grep -i flag", "description": "Extract strings"},
|
||||||
|
],
|
||||||
|
"pwn": [
|
||||||
|
{"name": "GDB + Pwndbg", "command": "gdb ./binary", "description": "Debugger with pwn extensions"},
|
||||||
|
{"name": "Pwntools", "command": "python3 -c 'from pwn import *'", "description": "CTF exploitation library"},
|
||||||
|
{"name": "ROPgadget", "command": "ROPgadget --binary ./binary", "description": "ROP chain builder"},
|
||||||
|
{"name": "Checksec", "command": "checksec --file=./binary", "description": "Binary security checks"},
|
||||||
|
{"name": "One_gadget", "command": "one_gadget libc.so.6", "description": "Find one-shot RCE gadgets"},
|
||||||
|
],
|
||||||
|
"reversing": [
|
||||||
|
{"name": "Ghidra", "command": "ghidra", "description": "NSA reverse engineering tool"},
|
||||||
|
{"name": "IDA Free", "command": "ida", "description": "Interactive disassembler"},
|
||||||
|
{"name": "Radare2", "command": "r2 -A ./binary", "description": "Reverse engineering framework"},
|
||||||
|
{"name": "Objdump", "command": "objdump -d ./binary", "description": "Disassembler"},
|
||||||
|
{"name": "Ltrace/Strace", "command": "ltrace ./binary", "description": "Library/system call tracer"},
|
||||||
|
],
|
||||||
|
"misc": [
|
||||||
|
{"name": "CyberChef", "command": "https://gchq.github.io/CyberChef/", "description": "Swiss army knife"},
|
||||||
|
{"name": "dCode", "command": "https://www.dcode.fr/", "description": "Cipher identifier"},
|
||||||
|
{"name": "Wireshark", "command": "wireshark capture.pcap", "description": "Network analysis"},
|
||||||
|
{"name": "Sonic Visualiser", "command": "sonic-visualiser", "description": "Audio analysis"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"category": category, "tools": tools.get(category, tools["misc"])}
|
||||||
|
|
||||||
|
|
||||||
|
def get_ctf_system_prompt(category: str) -> str:
|
||||||
|
"""Get specialized system prompt for CTF category"""
|
||||||
|
|
||||||
|
base_prompt = """You are an expert CTF (Capture The Flag) mentor and security researcher.
|
||||||
|
You help players learn and improve their skills through educational guidance.
|
||||||
|
Never give direct flag answers, but help them understand the methodology."""
|
||||||
|
|
||||||
|
category_prompts = {
|
||||||
|
"web": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Web Security & Application Exploitation
|
||||||
|
|
||||||
|
You're an expert in:
|
||||||
|
- SQL Injection (SQLi), XSS, CSRF, SSRF, XXE
|
||||||
|
- Authentication bypasses and session management
|
||||||
|
- File upload vulnerabilities and LFI/RFI
|
||||||
|
- Server-side template injection (SSTI)
|
||||||
|
- JWT vulnerabilities and OAuth attacks
|
||||||
|
- HTTP request smuggling
|
||||||
|
- WebSocket vulnerabilities
|
||||||
|
|
||||||
|
Always suggest checking: robots.txt, source code comments, HTTP headers, cookies.""",
|
||||||
|
|
||||||
|
"crypto": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Cryptography & Code Breaking
|
||||||
|
|
||||||
|
You're an expert in:
|
||||||
|
- Classical ciphers (Caesar, Vigenere, substitution)
|
||||||
|
- Modern crypto attacks (RSA, AES, DES weaknesses)
|
||||||
|
- Hash cracking and rainbow tables
|
||||||
|
- Encoding detection (Base64, hex, URL encoding)
|
||||||
|
- Padding oracle attacks
|
||||||
|
- XOR analysis and known-plaintext attacks
|
||||||
|
- Elliptic curve cryptography
|
||||||
|
|
||||||
|
Suggest tools: CyberChef, hashcat, john, RsaCtfTool, xortool.""",
|
||||||
|
|
||||||
|
"forensics": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Digital Forensics & Incident Response
|
||||||
|
|
||||||
|
You're an expert in:
|
||||||
|
- Memory forensics (Volatility framework)
|
||||||
|
- Disk image analysis
|
||||||
|
- File carving and recovery
|
||||||
|
- Network packet analysis (Wireshark)
|
||||||
|
- Steganography detection
|
||||||
|
- Metadata analysis
|
||||||
|
- Log analysis and timeline reconstruction
|
||||||
|
|
||||||
|
Always check: file signatures, hidden data, deleted files, metadata.""",
|
||||||
|
|
||||||
|
"pwn": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Binary Exploitation & Pwn
|
||||||
|
|
||||||
|
You're an expert in:
|
||||||
|
- Buffer overflows (stack, heap)
|
||||||
|
- Format string vulnerabilities
|
||||||
|
- Return-oriented programming (ROP)
|
||||||
|
- Shellcode development
|
||||||
|
- ASLR/NX/PIE/Stack canary bypasses
|
||||||
|
- Use-after-free and double-free
|
||||||
|
- Race conditions
|
||||||
|
|
||||||
|
Check protections first with checksec. Understand the binary before exploiting.""",
|
||||||
|
|
||||||
|
"reversing": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Reverse Engineering
|
||||||
|
|
||||||
|
You're an expert in:
|
||||||
|
- Static analysis (Ghidra, IDA, radare2)
|
||||||
|
- Dynamic analysis and debugging (GDB)
|
||||||
|
- Decompilation and code reconstruction
|
||||||
|
- Malware analysis techniques
|
||||||
|
- Obfuscation and anti-debugging bypasses
|
||||||
|
- Protocol reverse engineering
|
||||||
|
- Patching binaries
|
||||||
|
|
||||||
|
Start with strings, file type, and basic static analysis before dynamic.""",
|
||||||
|
|
||||||
|
"misc": f"""{base_prompt}
|
||||||
|
|
||||||
|
SPECIALTY: Miscellaneous CTF Challenges
|
||||||
|
|
||||||
|
You're versatile in:
|
||||||
|
- OSINT (Open Source Intelligence)
|
||||||
|
- Trivia and research challenges
|
||||||
|
- Programming and scripting puzzles
|
||||||
|
- Audio/image analysis
|
||||||
|
- QR codes and barcodes
|
||||||
|
- Esoteric programming languages
|
||||||
|
- Social engineering concepts
|
||||||
|
|
||||||
|
Think outside the box. Check for hidden messages in unusual places."""
|
||||||
|
}
|
||||||
|
|
||||||
|
return category_prompts.get(category, base_prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_ctf_tools(response: str, category: str) -> List[Dict]:
|
||||||
|
"""Extract tool suggestions from AI response"""
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
# Common tool patterns to look for
|
||||||
|
tool_patterns = {
|
||||||
|
"sqlmap": {"name": "SQLMap", "category": "web"},
|
||||||
|
"burp": {"name": "Burp Suite", "category": "web"},
|
||||||
|
"gobuster": {"name": "Gobuster", "category": "web"},
|
||||||
|
"nikto": {"name": "Nikto", "category": "web"},
|
||||||
|
"hashcat": {"name": "Hashcat", "category": "crypto"},
|
||||||
|
"john": {"name": "John the Ripper", "category": "crypto"},
|
||||||
|
"cyberchef": {"name": "CyberChef", "category": "crypto"},
|
||||||
|
"volatility": {"name": "Volatility", "category": "forensics"},
|
||||||
|
"binwalk": {"name": "Binwalk", "category": "forensics"},
|
||||||
|
"wireshark": {"name": "Wireshark", "category": "forensics"},
|
||||||
|
"ghidra": {"name": "Ghidra", "category": "reversing"},
|
||||||
|
"gdb": {"name": "GDB", "category": "pwn"},
|
||||||
|
"pwntools": {"name": "Pwntools", "category": "pwn"},
|
||||||
|
"checksec": {"name": "Checksec", "category": "pwn"},
|
||||||
|
}
|
||||||
|
|
||||||
|
response_lower = response.lower()
|
||||||
|
for pattern, tool_info in tool_patterns.items():
|
||||||
|
if pattern in response_lower:
|
||||||
|
tools.append(tool_info)
|
||||||
|
|
||||||
|
return tools[:5] # Return top 5
|
||||||
|
|
||||||
|
|
||||||
|
def get_follow_up_questions(category: str) -> List[str]:
|
||||||
|
"""Get relevant follow-up questions for a category"""
|
||||||
|
|
||||||
|
questions = {
|
||||||
|
"web": [
|
||||||
|
"What HTTP methods does the endpoint accept?",
|
||||||
|
"Have you checked for hidden parameters?",
|
||||||
|
"What does the source code reveal?",
|
||||||
|
],
|
||||||
|
"crypto": [
|
||||||
|
"What's the key length or block size?",
|
||||||
|
"Do you see any patterns in the ciphertext?",
|
||||||
|
"Is this a known algorithm or custom?",
|
||||||
|
],
|
||||||
|
"forensics": [
|
||||||
|
"What file type is it really (check magic bytes)?",
|
||||||
|
"Have you looked at the metadata?",
|
||||||
|
"Are there any hidden or deleted files?",
|
||||||
|
],
|
||||||
|
"pwn": [
|
||||||
|
"What protections are enabled (checksec)?",
|
||||||
|
"Is there a stack canary leak possible?",
|
||||||
|
"What libc version is being used?",
|
||||||
|
],
|
||||||
|
"reversing": [
|
||||||
|
"What's the program's main functionality?",
|
||||||
|
"Are there any anti-debugging tricks?",
|
||||||
|
"Have you identified the key functions?",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions.get(category, ["What have you tried so far?", "Can you share more details?"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_ctf_fallback(category: str, message: str) -> str:
|
||||||
|
"""Fallback response when AI is unavailable"""
|
||||||
|
|
||||||
|
fallbacks = {
|
||||||
|
"web": """For web challenges, start with:
|
||||||
|
|
||||||
|
1. **Reconnaissance**: Check robots.txt, sitemap.xml, source comments
|
||||||
|
2. **Identify the stack**: Server headers, error messages, file extensions
|
||||||
|
3. **Test inputs**: Try SQLi, XSS, command injection on all inputs
|
||||||
|
4. **Check auth**: Test for auth bypasses, weak sessions, JWT issues
|
||||||
|
|
||||||
|
🔧 Tools: Burp Suite, SQLMap, Gobuster, dirb""",
|
||||||
|
|
||||||
|
"crypto": """For crypto challenges:
|
||||||
|
|
||||||
|
1. **Identify the cipher**: Pattern analysis, key indicators
|
||||||
|
2. **Check encoding**: Base64? Hex? Multiple layers?
|
||||||
|
3. **Look for weaknesses**: Small keys, reused IVs, weak algorithms
|
||||||
|
4. **Use tools**: CyberChef for quick decoding
|
||||||
|
|
||||||
|
🔧 Tools: CyberChef, hashcat, john, dCode.fr""",
|
||||||
|
|
||||||
|
"forensics": """For forensics challenges:
|
||||||
|
|
||||||
|
1. **File analysis**: `file`, `binwalk`, `strings`, `xxd`
|
||||||
|
2. **Check metadata**: `exiftool`, hidden fields
|
||||||
|
3. **Steganography**: `steghide`, `zsteg`, `stegsolve`
|
||||||
|
4. **Memory/Disk**: Volatility, Autopsy, FTK
|
||||||
|
|
||||||
|
🔧 Tools: binwalk, foremost, volatility, Wireshark""",
|
||||||
|
|
||||||
|
"pwn": """For pwn challenges:
|
||||||
|
|
||||||
|
1. **Check protections**: `checksec ./binary`
|
||||||
|
2. **Understand the binary**: Run it, analyze with GDB
|
||||||
|
3. **Find vulnerabilities**: Buffer overflow? Format string?
|
||||||
|
4. **Build exploit**: Pwntools makes it easier
|
||||||
|
|
||||||
|
🔧 Tools: GDB + pwndbg, pwntools, ROPgadget, one_gadget""",
|
||||||
|
|
||||||
|
"reversing": """For reversing challenges:
|
||||||
|
|
||||||
|
1. **Static analysis**: `file`, `strings`, Ghidra/IDA
|
||||||
|
2. **Understand flow**: Find main(), trace execution
|
||||||
|
3. **Dynamic analysis**: GDB, ltrace, strace
|
||||||
|
4. **Patch if needed**: Modify jumps, bypass checks
|
||||||
|
|
||||||
|
🔧 Tools: Ghidra, IDA Free, radare2, GDB"""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbacks.get(category, """I'm your CTF assistant!
|
||||||
|
|
||||||
|
I can help with:
|
||||||
|
- 🌐 **Web**: SQLi, XSS, SSRF, auth bypasses
|
||||||
|
- 🔐 **Crypto**: Encoding, ciphers, hash cracking
|
||||||
|
- 🔍 **Forensics**: File analysis, memory, steganography
|
||||||
|
- 💥 **Pwn**: Buffer overflows, ROP chains
|
||||||
|
- 🔄 **Reversing**: Disassembly, debugging
|
||||||
|
|
||||||
|
What category is your challenge?""")
|
||||||
|
|
||||||
|
|
||||||
|
def get_fallback_hint(category: str, level: int) -> str:
|
||||||
|
"""Get fallback hints when AI unavailable"""
|
||||||
|
|
||||||
|
hints = {
|
||||||
|
1: "🔍 Look more carefully at the data you're given. There might be something hidden in plain sight.",
|
||||||
|
2: f"💡 For {category} challenges, make sure you've tried the standard tools and techniques for this category.",
|
||||||
|
3: f"🎯 Focus on the most common vulnerability types for {category}. Check the challenge description for subtle clues."
|
||||||
|
}
|
||||||
|
|
||||||
|
return hints.get(level, hints[2])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
156
services/dashboard/components/GuidedWizard.jsx
Normal file
156
services/dashboard/components/GuidedWizard.jsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const WIZARD_TYPES = {
|
||||||
|
first_time_setup: {
|
||||||
|
title: 'Welcome to GooseStrike',
|
||||||
|
steps: [
|
||||||
|
{ id: 'intro', title: 'Introduction', icon: '👋' },
|
||||||
|
{ id: 'phases', title: 'Methodology', icon: '📋' },
|
||||||
|
{ id: 'tools', title: 'Security Tools', icon: '🛠️' },
|
||||||
|
{ id: 'start', title: 'Get Started', icon: '🚀' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
run_scan: {
|
||||||
|
title: 'Run Security Scan',
|
||||||
|
steps: [
|
||||||
|
{ id: 'target', title: 'Target Selection', icon: '🎯' },
|
||||||
|
{ id: 'scan-type', title: 'Scan Type', icon: '🔍' },
|
||||||
|
{ id: 'options', title: 'Options', icon: '⚙️' },
|
||||||
|
{ id: 'execute', title: 'Execute', icon: '▶️' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
create_operation: {
|
||||||
|
title: 'Create Security Operation',
|
||||||
|
steps: [
|
||||||
|
{ id: 'details', title: 'Operation Details', icon: '📝' },
|
||||||
|
{ id: 'scope', title: 'Target Scope', icon: '🎯' },
|
||||||
|
{ id: 'methodology', title: 'Methodology', icon: '📋' },
|
||||||
|
{ id: 'review', title: 'Review', icon: '✅' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const GuidedWizard = ({ type = 'first_time_setup', onComplete = () => {}, onCancel = () => {} }) => {
|
||||||
|
const wizard = WIZARD_TYPES[type] || WIZARD_TYPES.first_time_setup;
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const [formData, setFormData] = useState({});
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (currentStep < wizard.steps.length - 1) {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
} else {
|
||||||
|
onComplete(formData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (currentStep > 0) {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (key, value) => {
|
||||||
|
setFormData({ ...formData, [key]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress = ((currentStep + 1) / wizard.steps.length) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="guided-wizard fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-sp-dark rounded-lg border border-sp-grey-mid w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-sp-grey-mid">
|
||||||
|
<h2 className="text-2xl font-bold text-sp-white">{wizard.title}</h2>
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
{wizard.steps.map((step, idx) => (
|
||||||
|
<div
|
||||||
|
key={step.id}
|
||||||
|
className={`wizard-step flex-1 p-2 rounded text-center border transition ${
|
||||||
|
idx === currentStep
|
||||||
|
? 'border-sp-red bg-sp-red bg-opacity-10'
|
||||||
|
: idx < currentStep
|
||||||
|
? 'border-green-500 bg-green-500 bg-opacity-10'
|
||||||
|
: 'border-sp-grey-mid'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-xl">{step.icon}</div>
|
||||||
|
<div className="text-xs text-sp-white-muted mt-1">{step.title}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 h-1 bg-sp-grey-mid rounded overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="wizard-progress h-full bg-sp-red transition-all duration-300"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="flex-1 p-6 overflow-y-auto">
|
||||||
|
<div className="text-sp-white">
|
||||||
|
{/* Render step content based on wizard type and current step */}
|
||||||
|
{type === 'first_time_setup' && currentStep === 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">Welcome to GooseStrike! 🍁</h3>
|
||||||
|
<p className="text-sp-white-muted mb-4">
|
||||||
|
GooseStrike is an AI-powered penetration testing platform that follows industry-standard
|
||||||
|
methodologies to help you identify security vulnerabilities.
|
||||||
|
</p>
|
||||||
|
<ul className="list-disc list-inside text-sp-white-muted space-y-2">
|
||||||
|
<li>AI-assisted security analysis with local or cloud LLMs</li>
|
||||||
|
<li>600+ integrated Kali Linux security tools</li>
|
||||||
|
<li>Voice control for hands-free operation</li>
|
||||||
|
<li>Interactive network visualization</li>
|
||||||
|
<li>Comprehensive reporting and documentation</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type === 'run_scan' && currentStep === 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold mb-4">Select Target</h3>
|
||||||
|
<label className="block mb-2 text-sm text-sp-white-muted">Target IP or hostname</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sp-white"
|
||||||
|
placeholder="192.168.1.100 or example.com"
|
||||||
|
value={formData.target || ''}
|
||||||
|
onChange={(e) => handleInputChange('target', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Add more step content as needed */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-6 border-t border-sp-grey-mid flex justify-between">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{currentStep > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
|
||||||
|
>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="px-4 py-2 bg-sp-red hover:bg-sp-red-dark rounded text-sp-white transition"
|
||||||
|
>
|
||||||
|
{currentStep === wizard.steps.length - 1 ? 'Complete' : 'Next →'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GuidedWizard;
|
||||||
110
services/dashboard/components/NetworkMap.jsx
Normal file
110
services/dashboard/components/NetworkMap.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import cytoscape from 'cytoscape';
|
||||||
|
|
||||||
|
const NetworkMap = ({ hosts = [], onHostSelect = () => {} }) => {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const cyRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current || hosts.length === 0) return;
|
||||||
|
|
||||||
|
// Build Cytoscape elements from hosts
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
hosts.forEach((host) => {
|
||||||
|
elements.push({
|
||||||
|
data: {
|
||||||
|
id: host.ip,
|
||||||
|
label: host.hostname || host.ip,
|
||||||
|
type: host.device_type || 'unknown',
|
||||||
|
os: host.os || 'unknown',
|
||||||
|
ports: host.ports || [],
|
||||||
|
},
|
||||||
|
classes: host.device_type || 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add edges for network relationships (simple example: connect all to a central gateway)
|
||||||
|
if (host.ip !== '192.168.1.1') {
|
||||||
|
elements.push({
|
||||||
|
data: {
|
||||||
|
id: `edge-${host.ip}`,
|
||||||
|
source: '192.168.1.1',
|
||||||
|
target: host.ip,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize Cytoscape
|
||||||
|
cyRef.current = cytoscape({
|
||||||
|
container: containerRef.current,
|
||||||
|
elements,
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-color': '#dc2626',
|
||||||
|
label: 'data(label)',
|
||||||
|
color: '#e5e5e5',
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'font-size': '10px',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node.router',
|
||||||
|
style: {
|
||||||
|
'background-color': '#3b82f6',
|
||||||
|
shape: 'diamond',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node.server',
|
||||||
|
style: {
|
||||||
|
'background-color': '#22c55e',
|
||||||
|
shape: 'rectangle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
width: 2,
|
||||||
|
'line-color': '#3a3a3a',
|
||||||
|
'target-arrow-color': '#3a3a3a',
|
||||||
|
'target-arrow-shape': 'triangle',
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: {
|
||||||
|
name: 'cose',
|
||||||
|
animate: true,
|
||||||
|
animationDuration: 500,
|
||||||
|
nodeDimensionsIncludeLabels: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle node clicks
|
||||||
|
cyRef.current.on('tap', 'node', (evt) => {
|
||||||
|
const node = evt.target;
|
||||||
|
onHostSelect(node.data());
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (cyRef.current) {
|
||||||
|
cyRef.current.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [hosts, onHostSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="network-map-container w-full h-full min-h-[500px] rounded border border-sp-grey-mid"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkMap;
|
||||||
124
services/dashboard/components/VoiceControls.jsx
Normal file
124
services/dashboard/components/VoiceControls.jsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => {
|
||||||
|
const [state, setState] = useState('idle'); // idle, listening, processing
|
||||||
|
const [transcript, setTranscript] = useState('');
|
||||||
|
const [hotkey, setHotkey] = useState('`'); // backtick
|
||||||
|
const mediaRecorderRef = useRef(null);
|
||||||
|
const audioChunksRef = useRef([]);
|
||||||
|
const hotkeyPressedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
|
||||||
|
hotkeyPressedRef.current = true;
|
||||||
|
startRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e) => {
|
||||||
|
if (e.key === hotkey && hotkeyPressedRef.current) {
|
||||||
|
hotkeyPressedRef.current = false;
|
||||||
|
stopRecording();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [state, hotkey]);
|
||||||
|
|
||||||
|
const startRecording = async () => {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||||
|
audioChunksRef.current = [];
|
||||||
|
|
||||||
|
mediaRecorderRef.current.ondataavailable = (event) => {
|
||||||
|
audioChunksRef.current.push(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorderRef.current.onstop = async () => {
|
||||||
|
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
||||||
|
await sendToTranscribe(audioBlob);
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorderRef.current.start();
|
||||||
|
setState('listening');
|
||||||
|
setTranscript('Listening...');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Microphone access denied:', error);
|
||||||
|
setTranscript('Microphone access denied');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (mediaRecorderRef.current && state === 'listening') {
|
||||||
|
mediaRecorderRef.current.stop();
|
||||||
|
setState('processing');
|
||||||
|
setTranscript('Processing...');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToTranscribe = async (audioBlob) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', audioBlob, 'recording.wav');
|
||||||
|
|
||||||
|
const response = await fetch(`${apiUrl}/transcribe`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
setTranscript(result.text || 'No speech detected');
|
||||||
|
setState('idle');
|
||||||
|
|
||||||
|
if (result.text) {
|
||||||
|
onCommand(result.text);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Transcription failed:', error);
|
||||||
|
setTranscript('Transcription failed');
|
||||||
|
setState('idle');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="voice-controls p-4 bg-sp-grey rounded border border-sp-grey-mid">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
className={`voice-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl transition ${
|
||||||
|
state === 'listening'
|
||||||
|
? 'bg-sp-red animate-pulse'
|
||||||
|
: state === 'processing'
|
||||||
|
? 'bg-yellow-500'
|
||||||
|
: 'bg-sp-grey-mid hover:bg-sp-red'
|
||||||
|
}`}
|
||||||
|
onMouseDown={startRecording}
|
||||||
|
onMouseUp={stopRecording}
|
||||||
|
disabled={state === 'processing'}
|
||||||
|
>
|
||||||
|
{state === 'listening' ? '🎙️' : state === 'processing' ? '⏳' : '🎤'}
|
||||||
|
</button>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm text-sp-white-muted">
|
||||||
|
{state === 'idle' && `Press & hold ${hotkey} or click to speak`}
|
||||||
|
{state === 'listening' && 'Release to stop recording'}
|
||||||
|
{state === 'processing' && 'Processing audio...'}
|
||||||
|
</div>
|
||||||
|
{transcript && (
|
||||||
|
<div className="text-sm text-sp-white mt-1 font-mono">{transcript}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VoiceControls;
|
||||||
40
services/dashboard/components/index.jsx
Normal file
40
services/dashboard/components/index.jsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import VoiceControls from './VoiceControls';
|
||||||
|
import NetworkMap from './NetworkMap';
|
||||||
|
import GuidedWizard from './GuidedWizard';
|
||||||
|
|
||||||
|
// Export components for external mounting
|
||||||
|
window.GooseStrikeComponents = {
|
||||||
|
VoiceControls,
|
||||||
|
NetworkMap,
|
||||||
|
GuidedWizard,
|
||||||
|
mount: {
|
||||||
|
voiceControls: (containerId, props = {}) => {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<VoiceControls {...props} />);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networkMap: (containerId, props = {}) => {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<NetworkMap {...props} />);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
guidedWizard: (containerId, props = {}) => {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container) {
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<GuidedWizard {...props} />);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { VoiceControls, NetworkMap, GuidedWizard };
|
||||||
19
services/dashboard/package.json
Normal file
19
services/dashboard/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "goosestrike-dashboard",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cytoscape": "^3.28.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"vite": "^5.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
services/dashboard/vite.config.js
Normal file
21
services/dashboard/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'static/dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
components: path.resolve(__dirname, 'components/index.jsx'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'components.js',
|
||||||
|
chunkFileNames: 'components-[name].js',
|
||||||
|
assetFileNames: 'components-[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ Executes commands in the Kali container via Docker SDK.
|
|||||||
"""
|
"""
|
||||||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
import docker
|
import docker
|
||||||
@@ -557,6 +558,23 @@ def _run_command_sync(container, command, working_dir):
|
|||||||
workdir=working_dir
|
workdir=working_dir
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.get("/stream/processes")
|
||||||
|
async def stream_running_processes():
|
||||||
|
"""Server-Sent Events stream of running security processes.
|
||||||
|
Emits JSON events with current process list every 5 seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def event_generator():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = await get_running_processes()
|
||||||
|
yield f"data: {json.dumps(data)}\n\n"
|
||||||
|
except Exception as e:
|
||||||
|
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||||
|
|
||||||
@app.post("/execute", response_model=CommandResult)
|
@app.post("/execute", response_model=CommandResult)
|
||||||
async def execute_command(request: CommandRequest):
|
async def execute_command(request: CommandRequest):
|
||||||
"""Execute a command in the Kali container."""
|
"""Execute a command in the Kali container."""
|
||||||
@@ -622,6 +640,52 @@ async def execute_command(request: CommandRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Execution error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Execution error: {str(e)}")
|
||||||
|
|
||||||
|
@app.websocket("/ws/execute/{command_id}")
|
||||||
|
async def websocket_execute(websocket: WebSocket, command_id: str):
|
||||||
|
"""WebSocket endpoint for streaming command output in real-time."""
|
||||||
|
await websocket.accept()
|
||||||
|
|
||||||
|
if command_id not in running_commands:
|
||||||
|
await websocket.send_json({"error": "Command not found"})
|
||||||
|
await websocket.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd_info = running_commands[command_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Stream output as it becomes available
|
||||||
|
last_stdout_len = 0
|
||||||
|
last_stderr_len = 0
|
||||||
|
|
||||||
|
while cmd_info["status"] == "running":
|
||||||
|
current_stdout = cmd_info.get("stdout", "")
|
||||||
|
current_stderr = cmd_info.get("stderr", "")
|
||||||
|
|
||||||
|
# Send new stdout
|
||||||
|
if len(current_stdout) > last_stdout_len:
|
||||||
|
new_stdout = current_stdout[last_stdout_len:]
|
||||||
|
await websocket.send_json({"type": "stdout", "data": new_stdout})
|
||||||
|
last_stdout_len = len(current_stdout)
|
||||||
|
|
||||||
|
# Send new stderr
|
||||||
|
if len(current_stderr) > last_stderr_len:
|
||||||
|
new_stderr = current_stderr[last_stderr_len:]
|
||||||
|
await websocket.send_json({"type": "stderr", "data": new_stderr})
|
||||||
|
last_stderr_len = len(current_stderr)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Send final status
|
||||||
|
await websocket.send_json({
|
||||||
|
"type": "complete",
|
||||||
|
"status": cmd_info["status"],
|
||||||
|
"exit_code": cmd_info.get("exit_code"),
|
||||||
|
"duration": cmd_info.get("duration_seconds"),
|
||||||
|
})
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
await websocket.close()
|
||||||
|
|
||||||
@app.post("/execute/async")
|
@app.post("/execute/async")
|
||||||
async def execute_command_async(request: CommandRequest):
|
async def execute_command_async(request: CommandRequest):
|
||||||
|
|||||||
Reference in New Issue
Block a user