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:
2025-12-28 21:29:59 -05:00
parent b971482bbd
commit af31caeacf
10 changed files with 1144 additions and 25 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View 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;

View 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;

View 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;

View 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 };

View 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"
}
}

View 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]',
},
},
},
});

View File

@@ -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):