diff --git a/docker-compose.yml b/docker-compose.yml index a0d52a0..7f3a1f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -67,13 +67,13 @@ services: environment: - OPENAI_API_KEY=${OPENAI_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - # Local Ollama on host machine (use host.docker.internal on Windows/Mac) - - OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://host.docker.internal:11434} + # Prefer local Ollama container for self-contained setup + - OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://strikepackage-ollama:11434} # Network Ollama instances (Dell LLM box with larger models) - OLLAMA_NETWORK_URLS=${OLLAMA_NETWORK_URLS:-http://192.168.1.50:11434} # Legacy single endpoint (fallback) - - OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://host.docker.internal:11434} - - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434} + - OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://strikepackage-ollama:11434} + - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://strikepackage-ollama:11434} # Load balancing: round-robin, random, failover - LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-failover} extra_hosts: @@ -81,6 +81,8 @@ services: networks: - strikepackage-net restart: unless-stopped + depends_on: + - ollama # Kali Linux - Security tools container kali: @@ -100,26 +102,25 @@ services: - NET_RAW restart: unless-stopped - # Ollama - Local LLM (disabled - using Dell LLM box at 192.168.1.50) - # Uncomment to use local Ollama instead - # ollama: - # image: ollama/ollama:latest - # container_name: strikepackage-ollama - # ports: - # - "11434:11434" - # volumes: - # - ollama-models:/root/.ollama - # networks: - # - strikepackage-net - # restart: unless-stopped - # # Uncomment for GPU support: - # # deploy: - # # resources: - # # reservations: - # # devices: - # # - driver: nvidia - # # count: all - # # capabilities: [gpu] + # Ollama - Local LLM + ollama: + image: ollama/ollama:latest + container_name: strikepackage-ollama + ports: + - "11434:11434" + volumes: + - ollama-models:/root/.ollama + networks: + - strikepackage-net + restart: unless-stopped + # GPU support (optional): uncomment if using NVIDIA GPU + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: all + # capabilities: [gpu] networks: strikepackage-net: diff --git a/services/dashboard/Dockerfile b/services/dashboard/Dockerfile index 7bc4d1e..b615f29 100644 --- a/services/dashboard/Dockerfile +++ b/services/dashboard/Dockerfile @@ -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 WORKDIR /app @@ -11,6 +22,9 @@ COPY app/ ./app/ COPY templates/ ./templates/ COPY static/ ./static/ +# Copy built components from builder stage +COPY --from=builder /build/static/dist/ ./static/dist/ + # Expose port EXPOSE 8080 diff --git a/services/dashboard/app/main.py b/services/dashboard/app/main.py index daa6927..4fb2679 100644 --- a/services/dashboard/app/main.py +++ b/services/dashboard/app/main.py @@ -432,7 +432,15 @@ async def health_check(): @app.get("/", response_class=HTMLResponse) async def index(request: Request): """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) @@ -475,6 +483,23 @@ async def get_running_processes(): except: 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") async def get_providers(): @@ -501,6 +526,44 @@ async def get_tools(): except httpx.ConnectError: 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") async def chat(message: ChatMessage): @@ -2077,6 +2140,513 @@ I can assist with: 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 '{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__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080) \ No newline at end of file diff --git a/services/dashboard/components/GuidedWizard.jsx b/services/dashboard/components/GuidedWizard.jsx new file mode 100644 index 0000000..cfdff4e --- /dev/null +++ b/services/dashboard/components/GuidedWizard.jsx @@ -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 ( +
+
+ {/* Header */} +
+

{wizard.title}

+
+ {wizard.steps.map((step, idx) => ( +
+
{step.icon}
+
{step.title}
+
+ ))} +
+
+
+
+
+ + {/* Body */} +
+
+ {/* Render step content based on wizard type and current step */} + {type === 'first_time_setup' && currentStep === 0 && ( +
+

Welcome to GooseStrike! 🍁

+

+ GooseStrike is an AI-powered penetration testing platform that follows industry-standard + methodologies to help you identify security vulnerabilities. +

+
    +
  • AI-assisted security analysis with local or cloud LLMs
  • +
  • 600+ integrated Kali Linux security tools
  • +
  • Voice control for hands-free operation
  • +
  • Interactive network visualization
  • +
  • Comprehensive reporting and documentation
  • +
+
+ )} + {type === 'run_scan' && currentStep === 0 && ( +
+

Select Target

+ + handleInputChange('target', e.target.value)} + /> +
+ )} + {/* Add more step content as needed */} +
+
+ + {/* Footer */} +
+ +
+ {currentStep > 0 && ( + + )} + +
+
+
+
+ ); +}; + +export default GuidedWizard; diff --git a/services/dashboard/components/NetworkMap.jsx b/services/dashboard/components/NetworkMap.jsx new file mode 100644 index 0000000..4904629 --- /dev/null +++ b/services/dashboard/components/NetworkMap.jsx @@ -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 ( +
+ ); +}; + +export default NetworkMap; diff --git a/services/dashboard/components/VoiceControls.jsx b/services/dashboard/components/VoiceControls.jsx new file mode 100644 index 0000000..9647ae4 --- /dev/null +++ b/services/dashboard/components/VoiceControls.jsx @@ -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 ( +
+
+ +
+
+ {state === 'idle' && `Press & hold ${hotkey} or click to speak`} + {state === 'listening' && 'Release to stop recording'} + {state === 'processing' && 'Processing audio...'} +
+ {transcript && ( +
{transcript}
+ )} +
+
+
+ ); +}; + +export default VoiceControls; diff --git a/services/dashboard/components/index.jsx b/services/dashboard/components/index.jsx new file mode 100644 index 0000000..8905e6e --- /dev/null +++ b/services/dashboard/components/index.jsx @@ -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(); + return root; + } + }, + networkMap: (containerId, props = {}) => { + const container = document.getElementById(containerId); + if (container) { + const root = createRoot(container); + root.render(); + return root; + } + }, + guidedWizard: (containerId, props = {}) => { + const container = document.getElementById(containerId); + if (container) { + const root = createRoot(container); + root.render(); + return root; + } + }, + }, +}; + +export { VoiceControls, NetworkMap, GuidedWizard }; diff --git a/services/dashboard/package.json b/services/dashboard/package.json new file mode 100644 index 0000000..a7b6151 --- /dev/null +++ b/services/dashboard/package.json @@ -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" + } +} diff --git a/services/dashboard/vite.config.js b/services/dashboard/vite.config.js new file mode 100644 index 0000000..b61feb0 --- /dev/null +++ b/services/dashboard/vite.config.js @@ -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]', + }, + }, + }, +}); diff --git a/services/kali-executor/app/main.py b/services/kali-executor/app/main.py index 2736298..1024353 100644 --- a/services/kali-executor/app/main.py +++ b/services/kali-executor/app/main.py @@ -4,6 +4,7 @@ Executes commands in the Kali container via Docker SDK. """ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from typing import Optional, Dict, Any, List import docker @@ -557,6 +558,23 @@ def _run_command_sync(container, command, 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) async def execute_command(request: CommandRequest): """Execute a command in the Kali container.""" @@ -622,6 +640,52 @@ async def execute_command(request: CommandRequest): except Exception as 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") async def execute_command_async(request: CommandRequest):