diff --git a/services/dashboard/app/main.py b/services/dashboard/app/main.py index eb96962..daa6927 100644 --- a/services/dashboard/app/main.py +++ b/services/dashboard/app/main.py @@ -12,6 +12,60 @@ from typing import Optional, Dict, Any, List import httpx import os import json +import uuid +from datetime import datetime +from pathlib import Path + +# Project data storage +PROJECTS_DIR = Path("/app/data/projects") +PROJECTS_DIR.mkdir(parents=True, exist_ok=True) + +# In-memory project cache (loaded from disk) +projects_db: Dict[str, Dict] = {} +current_project_id: Optional[str] = None + + +def load_projects(): + """Load all projects from disk on startup""" + global projects_db + for project_file in PROJECTS_DIR.glob("*.json"): + try: + with open(project_file, 'r') as f: + project = json.load(f) + projects_db[project['id']] = project + except Exception as e: + print(f"Failed to load project {project_file}: {e}") + + +def save_project(project_id: str): + """Save a project to disk""" + if project_id in projects_db: + project_file = PROJECTS_DIR / f"{project_id}.json" + with open(project_file, 'w') as f: + json.dump(projects_db[project_id], f, indent=2, default=str) + + +def get_current_project() -> Optional[Dict]: + """Get the current active project""" + if current_project_id and current_project_id in projects_db: + return projects_db[current_project_id] + return None + + +def add_to_project(category: str, data: Dict): + """Add data to the current project""" + project = get_current_project() + if project: + if category not in project: + project[category] = [] + data['timestamp'] = datetime.now().isoformat() + project[category].append(data) + project['updated_at'] = datetime.now().isoformat() + save_project(project['id']) + + +# Load existing projects on startup +load_projects() app = FastAPI( title="StrikePackageGPT Dashboard", @@ -79,6 +133,296 @@ class NetworkScanRequest(BaseModel): scan_type: str = "os" # ping, quick, os, full +# Project Management Models +class ProjectCreate(BaseModel): + name: str + description: Optional[str] = "" + target_network: Optional[str] = None + scope: Optional[List[str]] = Field(default_factory=list) + + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + target_network: Optional[str] = None + scope: Optional[List[str]] = None + notes: Optional[str] = None + + +class CredentialCreate(BaseModel): + username: str + password: Optional[str] = None + hash: Optional[str] = None + hash_type: Optional[str] = None + domain: Optional[str] = None + host: Optional[str] = None + service: Optional[str] = None + source: Optional[str] = None + valid: Optional[bool] = None + notes: Optional[str] = None + + +class FindingCreate(BaseModel): + title: str + severity: str = "info" # critical, high, medium, low, info + host: Optional[str] = None + port: Optional[int] = None + service: Optional[str] = None + description: Optional[str] = None + evidence: Optional[str] = None + recommendation: Optional[str] = None + cve: Optional[str] = None + cvss: Optional[float] = None + tool: Optional[str] = None + + +class NoteCreate(BaseModel): + title: str + content: str + category: Optional[str] = "general" # general, recon, exploitation, post-exploit, loot + host: Optional[str] = None + + +# ============= PROJECT MANAGEMENT ENDPOINTS ============= + +@app.get("/api/projects") +async def list_projects(): + """List all projects""" + return { + "projects": list(projects_db.values()), + "current_project_id": current_project_id + } + + +@app.post("/api/projects") +async def create_project(project: ProjectCreate): + """Create a new project""" + global current_project_id + + project_id = str(uuid.uuid4())[:8] + now = datetime.now().isoformat() + + new_project = { + "id": project_id, + "name": project.name, + "description": project.description, + "target_network": project.target_network, + "scope": project.scope, + "created_at": now, + "updated_at": now, + "hosts": [], + "credentials": [], + "findings": [], + "scans": [], + "notes": [], + "sessions": [], + "evidence": [], + "attack_chains": [] + } + + projects_db[project_id] = new_project + save_project(project_id) + + # Auto-select new project + current_project_id = project_id + + return {"status": "success", "project": new_project} + + +@app.get("/api/projects/{project_id}") +async def get_project(project_id: str): + """Get a specific project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + return projects_db[project_id] + + +@app.put("/api/projects/{project_id}") +async def update_project(project_id: str, update: ProjectUpdate): + """Update a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + update_data = update.dict(exclude_unset=True) + + for key, value in update_data.items(): + if value is not None: + project[key] = value + + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success", "project": project} + + +@app.delete("/api/projects/{project_id}") +async def delete_project(project_id: str): + """Delete a project""" + global current_project_id + + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + # Remove from memory + del projects_db[project_id] + + # Remove from disk + project_file = PROJECTS_DIR / f"{project_id}.json" + if project_file.exists(): + project_file.unlink() + + # Update current project if needed + if current_project_id == project_id: + current_project_id = next(iter(projects_db), None) + + return {"status": "success", "message": "Project deleted"} + + +@app.post("/api/projects/{project_id}/select") +async def select_project(project_id: str): + """Select active project""" + global current_project_id + + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + current_project_id = project_id + return {"status": "success", "current_project_id": current_project_id} + + +@app.get("/api/projects/current") +async def get_current_project_api(): + """Get the currently selected project""" + if not current_project_id or current_project_id not in projects_db: + return {"project": None} + return {"project": projects_db[current_project_id]} + + +# Project Data Endpoints +@app.post("/api/projects/{project_id}/credentials") +async def add_credential(project_id: str, cred: CredentialCreate): + """Add a credential to a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + cred_data = cred.dict() + cred_data['id'] = str(uuid.uuid4())[:8] + cred_data['created_at'] = datetime.now().isoformat() + + project['credentials'].append(cred_data) + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success", "credential": cred_data} + + +@app.get("/api/projects/{project_id}/credentials") +async def list_credentials(project_id: str): + """List all credentials in a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + return {"credentials": projects_db[project_id].get('credentials', [])} + + +@app.delete("/api/projects/{project_id}/credentials/{cred_id}") +async def delete_credential(project_id: str, cred_id: str): + """Delete a credential from a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + project['credentials'] = [c for c in project.get('credentials', []) if c.get('id') != cred_id] + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success"} + + +@app.post("/api/projects/{project_id}/findings") +async def add_finding(project_id: str, finding: FindingCreate): + """Add a finding to a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + finding_data = finding.dict() + finding_data['id'] = str(uuid.uuid4())[:8] + finding_data['created_at'] = datetime.now().isoformat() + + project['findings'].append(finding_data) + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success", "finding": finding_data} + + +@app.get("/api/projects/{project_id}/findings") +async def list_findings(project_id: str): + """List all findings in a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + return {"findings": projects_db[project_id].get('findings', [])} + + +@app.delete("/api/projects/{project_id}/findings/{finding_id}") +async def delete_finding(project_id: str, finding_id: str): + """Delete a finding from a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + project['findings'] = [f for f in project.get('findings', []) if f.get('id') != finding_id] + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success"} + + +@app.post("/api/projects/{project_id}/notes") +async def add_note(project_id: str, note: NoteCreate): + """Add a note to a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + note_data = note.dict() + note_data['id'] = str(uuid.uuid4())[:8] + note_data['created_at'] = datetime.now().isoformat() + + project['notes'].append(note_data) + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success", "note": note_data} + + +@app.get("/api/projects/{project_id}/notes") +async def list_notes(project_id: str): + """List all notes in a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + return {"notes": projects_db[project_id].get('notes', [])} + + +@app.delete("/api/projects/{project_id}/notes/{note_id}") +async def delete_note(project_id: str, note_id: str): + """Delete a note from a project""" + if project_id not in projects_db: + raise HTTPException(status_code=404, detail="Project not found") + + project = projects_db[project_id] + project['notes'] = [n for n in project.get('notes', []) if n.get('id') != note_id] + project['updated_at'] = datetime.now().isoformat() + save_project(project_id) + + return {"status": "success"} + + +# ============= END PROJECT MANAGEMENT ============= + + @app.get("/health") async def health_check(): """Health check endpoint""" @@ -828,7 +1172,10 @@ async def get_network_scan(scan_id: str): @app.get("/api/network/hosts") async def get_network_hosts(): - """Get all discovered network hosts""" + """Get all discovered network hosts (from current project if selected)""" + project = get_current_project() + if project: + return {"hosts": project.get('hosts', []), "project_id": project['id']} return {"hosts": network_hosts} @@ -843,6 +1190,9 @@ async def discover_hosts(request: HostDiscoveryRequest): """Add hosts discovered from terminal commands (e.g., nmap scans)""" global network_hosts + project = get_current_project() + hosts_list = project.get('hosts', []) if project else network_hosts + added = 0 updated = 0 @@ -860,7 +1210,7 @@ async def discover_hosts(request: HostDiscoveryRequest): host.setdefault("source", request.source) # Check if host already exists - existing = next((h for h in network_hosts if h["ip"] == host["ip"]), None) + existing = next((h for h in hosts_list if h["ip"] == host["ip"]), None) if existing: # Update existing host - merge ports existing_ports = {(p["port"], p.get("protocol", "tcp")) for p in existing.get("ports", [])} @@ -876,14 +1226,23 @@ async def discover_hosts(request: HostDiscoveryRequest): updated += 1 else: - network_hosts.append(host) + hosts_list.append(host) + if not project: + network_hosts.append(host) added += 1 + # Save to project if active + if project: + project['hosts'] = hosts_list + project['updated_at'] = datetime.now().isoformat() + save_project(project['id']) + return { "status": "success", "added": added, "updated": updated, - "total_hosts": len(network_hosts) + "total_hosts": len(hosts_list), + "project_id": project['id'] if project else None } @@ -891,6 +1250,15 @@ async def discover_hosts(request: HostDiscoveryRequest): async def clear_network_hosts(): """Clear all discovered network hosts""" global network_hosts + + project = get_current_project() + if project: + count = len(project.get('hosts', [])) + project['hosts'] = [] + project['updated_at'] = datetime.now().isoformat() + save_project(project['id']) + return {"status": "success", "cleared": count, "project_id": project['id']} + count = len(network_hosts) network_hosts = [] return {"status": "success", "cleared": count} @@ -900,6 +1268,19 @@ async def clear_network_hosts(): async def delete_network_host(ip: str): """Delete a specific network host by IP""" global network_hosts + + project = get_current_project() + if project: + hosts = project.get('hosts', []) + original_count = len(hosts) + project['hosts'] = [h for h in hosts if h.get("ip") != ip] + + if len(project['hosts']) < original_count: + project['updated_at'] = datetime.now().isoformat() + save_project(project['id']) + return {"status": "success", "deleted": ip} + raise HTTPException(status_code=404, detail=f"Host {ip} not found") + original_count = len(network_hosts) network_hosts = [h for h in network_hosts if h.get("ip") != ip] @@ -1071,6 +1452,546 @@ def get_local_explanation(type: str, context: Dict[str, Any]) -> Dict[str, Any]: } +# =========================================== +# Exploit Suggestion Engine +# =========================================== +class ExploitSuggestionRequest(BaseModel): + host_ip: Optional[str] = None + service: Optional[str] = None + version: Optional[str] = None + port: Optional[int] = None + os_type: Optional[str] = None + all_hosts: bool = False # If true, suggest for all project hosts + + +@app.post("/api/exploits/suggest") +async def suggest_exploits(request: ExploitSuggestionRequest): + """Get AI-powered exploit suggestions based on discovered services""" + project = get_current_project() + + suggestions = [] + + # Build target list + targets = [] + if request.all_hosts and project: + targets = project.get('hosts', []) + elif request.host_ip and project: + targets = [h for h in project.get('hosts', []) if h.get('ip') == request.host_ip] + elif request.service or request.version: + # Create synthetic target for service-based query + targets = [{ + 'ip': request.host_ip or 'target', + 'ports': [{'port': request.port or 0, 'service': request.service, 'version': request.version}], + 'os_type': request.os_type or '' + }] + + for host in targets: + host_suggestions = await get_exploit_suggestions_for_host(host) + if host_suggestions: + suggestions.extend(host_suggestions) + + # Deduplicate and sort by severity + seen = set() + unique_suggestions = [] + for s in suggestions: + key = (s.get('exploit_name', ''), s.get('target', '')) + if key not in seen: + seen.add(key) + unique_suggestions.append(s) + + severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'info': 4} + unique_suggestions.sort(key=lambda x: severity_order.get(x.get('severity', 'info'), 4)) + + return { + "suggestions": unique_suggestions[:20], # Top 20 + "total": len(unique_suggestions), + "hosts_analyzed": len(targets) + } + + +async def get_exploit_suggestions_for_host(host: Dict) -> List[Dict]: + """Get exploit suggestions for a specific host""" + suggestions = [] + host_ip = host.get('ip', 'unknown') + os_type = host.get('os_type', '').lower() + + for port_info in host.get('ports', []): + port = port_info.get('port') + service = port_info.get('service', '').lower() + version = port_info.get('version', '') + product = port_info.get('product', '') + + # Get known exploits for this service + port_exploits = get_known_exploits(port, service, version, product, os_type) + for exploit in port_exploits: + exploit['target'] = host_ip + exploit['target_port'] = port + exploit['target_service'] = service + suggestions.append(exploit) + + return suggestions + + +def get_known_exploits(port: int, service: str, version: str, product: str, os_type: str) -> List[Dict]: + """Return known exploits for service/version combinations""" + exploits = [] + + # SMB Exploits + if port == 445 or 'smb' in service or 'microsoft-ds' in service: + exploits.extend([ + { + 'exploit_name': 'EternalBlue (MS17-010)', + 'cve': 'CVE-2017-0144', + 'severity': 'critical', + 'description': 'Remote code execution in SMBv1. Devastating for Windows 7/Server 2008.', + 'msf_module': 'exploit/windows/smb/ms17_010_eternalblue', + 'manual_check': 'nmap -p445 --script smb-vuln-ms17-010 TARGET', + 'conditions': 'Windows systems with SMBv1 enabled' + }, + { + 'exploit_name': 'SMB Ghost (CVE-2020-0796)', + 'cve': 'CVE-2020-0796', + 'severity': 'critical', + 'description': 'RCE in SMBv3 compression. Affects Windows 10 1903/1909.', + 'msf_module': 'auxiliary/scanner/smb/smb_ms17_010', + 'manual_check': 'nmap -p445 --script smb-vuln-cve-2020-0796 TARGET', + 'conditions': 'Windows 10 version 1903 or 1909' + }, + { + 'exploit_name': 'SMB Null Session Enumeration', + 'cve': None, + 'severity': 'medium', + 'description': 'Anonymous access to enumerate users, shares, and policies.', + 'msf_module': 'auxiliary/scanner/smb/smb_enumshares', + 'manual_check': 'enum4linux -a TARGET', + 'conditions': 'Misconfigured SMB allowing null sessions' + } + ]) + + # SSH Exploits + if port == 22 or 'ssh' in service: + if 'openssh' in product.lower(): + if version: + try: + ver_num = float(version.split('.')[0] + '.' + version.split('.')[1].split('p')[0]) + if ver_num < 7.7: + exploits.append({ + 'exploit_name': 'OpenSSH User Enumeration', + 'cve': 'CVE-2018-15473', + 'severity': 'low', + 'description': f'OpenSSH {version} may be vulnerable to user enumeration.', + 'msf_module': 'auxiliary/scanner/ssh/ssh_enumusers', + 'manual_check': 'nmap -p22 --script ssh-auth-methods TARGET', + 'conditions': 'OpenSSH < 7.7' + }) + except: pass + + exploits.append({ + 'exploit_name': 'SSH Credential Brute Force', + 'cve': None, + 'severity': 'info', + 'description': 'Attempt common username/password combinations.', + 'msf_module': 'auxiliary/scanner/ssh/ssh_login', + 'manual_check': 'hydra -L users.txt -P passwords.txt ssh://TARGET', + 'conditions': 'Weak or default credentials' + }) + + # HTTP/HTTPS Exploits + if port in [80, 443, 8080, 8443] or 'http' in service: + exploits.extend([ + { + 'exploit_name': 'Web Application Scanning', + 'cve': None, + 'severity': 'info', + 'description': 'Automated scan for common web vulnerabilities.', + 'msf_module': None, + 'manual_check': 'nikto -h TARGET:' + str(port), + 'conditions': 'Web application present' + }, + { + 'exploit_name': 'Directory Enumeration', + 'cve': None, + 'severity': 'info', + 'description': 'Find hidden directories and files.', + 'msf_module': None, + 'manual_check': f'gobuster dir -u http://TARGET:{port} -w /usr/share/wordlists/dirb/common.txt', + 'conditions': 'Web server responding' + } + ]) + + # Apache/nginx specific + if 'apache' in product.lower(): + exploits.append({ + 'exploit_name': 'Apache mod_cgi RCE (Shellshock)', + 'cve': 'CVE-2014-6271', + 'severity': 'critical', + 'description': 'Remote code execution via CGI scripts if Bash is vulnerable.', + 'msf_module': 'exploit/multi/http/apache_mod_cgi_bash_env_exec', + 'manual_check': 'nmap -p' + str(port) + ' --script http-shellshock TARGET', + 'conditions': 'Apache with CGI and vulnerable Bash' + }) + + # FTP Exploits + if port == 21 or 'ftp' in service: + exploits.extend([ + { + 'exploit_name': 'FTP Anonymous Access', + 'cve': None, + 'severity': 'medium', + 'description': 'Anonymous FTP login may expose sensitive files.', + 'msf_module': 'auxiliary/scanner/ftp/anonymous', + 'manual_check': 'nmap -p21 --script ftp-anon TARGET', + 'conditions': 'Anonymous access enabled' + } + ]) + + if 'vsftpd' in product.lower() and '2.3.4' in version: + exploits.append({ + 'exploit_name': 'vsFTPd 2.3.4 Backdoor', + 'cve': 'CVE-2011-2523', + 'severity': 'critical', + 'description': 'Backdoor command execution in vsFTPd 2.3.4.', + 'msf_module': 'exploit/unix/ftp/vsftpd_234_backdoor', + 'manual_check': 'Connect to FTP with username containing :)', + 'conditions': 'vsFTPd version 2.3.4 exactly' + }) + + # RDP Exploits + if port == 3389 or 'rdp' in service or 'ms-wbt-server' in service: + exploits.extend([ + { + 'exploit_name': 'BlueKeep (CVE-2019-0708)', + 'cve': 'CVE-2019-0708', + 'severity': 'critical', + 'description': 'Pre-authentication RCE in RDP. Wormable vulnerability.', + 'msf_module': 'exploit/windows/rdp/cve_2019_0708_bluekeep_rce', + 'manual_check': 'nmap -p3389 --script rdp-vuln-ms12-020 TARGET', + 'conditions': 'Windows 7, Server 2008, Server 2008 R2' + }, + { + 'exploit_name': 'RDP Credential Brute Force', + 'cve': None, + 'severity': 'info', + 'description': 'Attempt common credentials against RDP.', + 'msf_module': 'auxiliary/scanner/rdp/rdp_scanner', + 'manual_check': 'hydra -L users.txt -P passwords.txt rdp://TARGET', + 'conditions': 'Weak or default credentials' + } + ]) + + # MySQL + if port == 3306 or 'mysql' in service: + exploits.extend([ + { + 'exploit_name': 'MySQL Default/Weak Credentials', + 'cve': None, + 'severity': 'high', + 'description': 'Check for root with no password or common credentials.', + 'msf_module': 'auxiliary/scanner/mysql/mysql_login', + 'manual_check': 'mysql -h TARGET -u root', + 'conditions': 'Weak authentication configuration' + }, + { + 'exploit_name': 'MySQL UDF for Command Execution', + 'cve': None, + 'severity': 'high', + 'description': 'If we have MySQL root, can potentially execute OS commands.', + 'msf_module': 'exploit/multi/mysql/mysql_udf_payload', + 'manual_check': 'After auth: SELECT sys_exec("whoami")', + 'conditions': 'MySQL root access with FILE privilege' + } + ]) + + # PostgreSQL + if port == 5432 or 'postgresql' in service: + exploits.append({ + 'exploit_name': 'PostgreSQL Default/Trust Authentication', + 'cve': None, + 'severity': 'high', + 'description': 'Check for default credentials or trust authentication.', + 'msf_module': 'auxiliary/scanner/postgres/postgres_login', + 'manual_check': 'psql -h TARGET -U postgres', + 'conditions': 'Default or trust authentication' + }) + + # Redis + if port == 6379 or 'redis' in service: + exploits.append({ + 'exploit_name': 'Redis Unauthenticated Access', + 'cve': None, + 'severity': 'critical', + 'description': 'Redis often runs without auth, allowing RCE via various techniques.', + 'msf_module': 'auxiliary/scanner/redis/redis_server', + 'manual_check': 'redis-cli -h TARGET INFO', + 'conditions': 'No authentication required' + }) + + # Telnet + if port == 23 or 'telnet' in service: + exploits.append({ + 'exploit_name': 'Telnet Service Active', + 'cve': None, + 'severity': 'medium', + 'description': 'Telnet transmits credentials in cleartext. Finding by itself.', + 'msf_module': 'auxiliary/scanner/telnet/telnet_login', + 'manual_check': 'telnet TARGET', + 'conditions': 'Telnet service running' + }) + + return exploits + + +@app.get("/api/exploits/search/{query}") +async def search_exploits(query: str): + """Search for exploits using searchsploit""" + try: + async with httpx.AsyncClient() as client: + response = await client.post( + f"{KALI_EXECUTOR_URL}/execute", + json={ + "command": f"searchsploit --json '{query}'", + "timeout": 30 + }, + timeout=35.0 + ) + + if response.status_code == 200: + data = response.json() + output = data.get('output', '') + + # Try to parse JSON output + try: + import json as json_module + # searchsploit --json output + exploits_data = json_module.loads(output) + return { + "query": query, + "results": exploits_data.get('RESULTS_EXPLOIT', [])[:20], + "total": len(exploits_data.get('RESULTS_EXPLOIT', [])) + } + except: + # Return raw output if not JSON + return { + "query": query, + "raw_output": output, + "results": [] + } + except Exception as e: + return {"query": query, "error": str(e), "results": []} + + +# =========================================== +# Automated Recon Pipeline +# =========================================== +class ReconPipelineRequest(BaseModel): + target: str # IP, range, or hostname + pipeline: str = "standard" # standard, quick, full, stealth + include_vuln_scan: bool = False + include_web_enum: bool = False + + +# Store active pipelines +active_pipelines: Dict[str, Dict] = {} + + +@app.post("/api/recon/pipeline") +async def start_recon_pipeline(request: ReconPipelineRequest): + """Start an automated recon pipeline""" + import asyncio + + pipeline_id = str(uuid.uuid4())[:8] + + # Define pipeline stages based on type + stages = [] + + if request.pipeline == "quick": + stages = [ + {"name": "Host Discovery", "command": f"nmap -sn -T4 --disable-arp-ping {request.target}", "timeout": 120}, + {"name": "Quick Port Scan", "command": f"nmap -sS -T4 -Pn --disable-arp-ping -F {request.target}", "timeout": 300}, + ] + elif request.pipeline == "stealth": + stages = [ + {"name": "Stealth Discovery", "command": f"nmap -sn -T2 {request.target}", "timeout": 300}, + {"name": "Slow Port Scan", "command": f"nmap -sS -T2 -Pn -p 21,22,23,25,80,443,445,3389 {request.target}", "timeout": 600}, + ] + elif request.pipeline == "full": + stages = [ + {"name": "Host Discovery", "command": f"nmap -sn -T4 --disable-arp-ping {request.target}", "timeout": 120}, + {"name": "Full Port Scan", "command": f"nmap -sS -T4 -Pn --disable-arp-ping -p- {request.target}", "timeout": 1800}, + {"name": "Service Detection", "command": f"nmap -sV -sC -T4 -Pn --disable-arp-ping -p- {request.target}", "timeout": 2400}, + {"name": "OS Detection", "command": f"nmap -O -T4 -Pn --disable-arp-ping {request.target}", "timeout": 600}, + ] + else: # standard + stages = [ + {"name": "Host Discovery", "command": f"nmap -sn -T4 --disable-arp-ping {request.target}", "timeout": 120}, + {"name": "Top Ports Scan", "command": f"nmap -sS -T4 -Pn --disable-arp-ping --top-ports 1000 {request.target}", "timeout": 600}, + {"name": "Service Detection", "command": f"nmap -sV -T4 -Pn --disable-arp-ping --top-ports 1000 {request.target}", "timeout": 900}, + {"name": "OS Detection", "command": f"nmap -O -T4 -Pn --disable-arp-ping {request.target}", "timeout": 600}, + ] + + # Add optional stages + if request.include_vuln_scan: + stages.append({ + "name": "Vulnerability Scan", + "command": f"nmap --script vuln -T4 -Pn --disable-arp-ping {request.target}", + "timeout": 1800 + }) + + if request.include_web_enum: + stages.append({ + "name": "Web Enumeration", + "command": f"nikto -h {request.target} -Format txt 2>/dev/null || echo 'Nikto scan complete'", + "timeout": 900 + }) + + # Initialize pipeline tracking + active_pipelines[pipeline_id] = { + "id": pipeline_id, + "target": request.target, + "pipeline": request.pipeline, + "stages": stages, + "current_stage": 0, + "status": "running", + "started_at": datetime.now().isoformat(), + "results": [], + "hosts_discovered": [] + } + + # Start pipeline execution in background + asyncio.create_task(execute_pipeline(pipeline_id)) + + return { + "status": "started", + "pipeline_id": pipeline_id, + "stages": len(stages), + "estimated_time": sum(s["timeout"] for s in stages) // 60, + "message": f"Pipeline '{request.pipeline}' started with {len(stages)} stages" + } + + +async def execute_pipeline(pipeline_id: str): + """Execute pipeline stages sequentially""" + pipeline = active_pipelines.get(pipeline_id) + if not pipeline: + return + + for i, stage in enumerate(pipeline["stages"]): + pipeline["current_stage"] = i + pipeline["current_stage_name"] = stage["name"] + + try: + async with httpx.AsyncClient() as client: + response = await client.post( + f"{KALI_EXECUTOR_URL}/execute", + json={ + "command": stage["command"], + "timeout": stage["timeout"], + "parse_output": True + }, + timeout=float(stage["timeout"] + 30) + ) + + if response.status_code == 200: + data = response.json() + result = { + "stage": stage["name"], + "command": stage["command"], + "success": True, + "output": data.get("output", ""), + "parsed": data.get("parsed", {}), + "completed_at": datetime.now().isoformat() + } + + # Extract hosts from parsed data + if data.get("parsed", {}).get("hosts"): + for host in data["parsed"]["hosts"]: + if host not in pipeline["hosts_discovered"]: + pipeline["hosts_discovered"].append(host) + + # Add to current project if selected + project = get_current_project() + if project: + existing = next((h for h in project.get('hosts', []) if h.get('ip') == host.get('ip')), None) + if not existing: + project['hosts'].append(host) + + pipeline["results"].append(result) + else: + pipeline["results"].append({ + "stage": stage["name"], + "success": False, + "error": f"Request failed: {response.status_code}" + }) + + except Exception as e: + pipeline["results"].append({ + "stage": stage["name"], + "success": False, + "error": str(e) + }) + + # Mark pipeline complete + pipeline["status"] = "completed" + pipeline["completed_at"] = datetime.now().isoformat() + + # Save project if active + project = get_current_project() + if project: + project['updated_at'] = datetime.now().isoformat() + save_project(project['id']) + + +@app.get("/api/recon/pipeline/{pipeline_id}") +async def get_pipeline_status(pipeline_id: str): + """Get status of a recon pipeline""" + if pipeline_id not in active_pipelines: + raise HTTPException(status_code=404, detail="Pipeline not found") + + pipeline = active_pipelines[pipeline_id] + return { + "id": pipeline["id"], + "target": pipeline["target"], + "status": pipeline["status"], + "current_stage": pipeline["current_stage"], + "current_stage_name": pipeline.get("current_stage_name", ""), + "total_stages": len(pipeline["stages"]), + "progress": (pipeline["current_stage"] + 1) / len(pipeline["stages"]) * 100 if pipeline["stages"] else 0, + "hosts_discovered": len(pipeline["hosts_discovered"]), + "started_at": pipeline["started_at"], + "completed_at": pipeline.get("completed_at"), + "results": pipeline["results"] + } + + +@app.get("/api/recon/pipelines") +async def list_pipelines(): + """List all recon pipelines""" + return { + "pipelines": [ + { + "id": p["id"], + "target": p["target"], + "pipeline": p["pipeline"], + "status": p["status"], + "progress": (p["current_stage"] + 1) / len(p["stages"]) * 100 if p["stages"] else 0, + "hosts_discovered": len(p["hosts_discovered"]), + "started_at": p["started_at"] + } + for p in active_pipelines.values() + ] + } + + +@app.delete("/api/recon/pipeline/{pipeline_id}") +async def cancel_pipeline(pipeline_id: str): + """Cancel a running pipeline""" + if pipeline_id not in active_pipelines: + raise HTTPException(status_code=404, detail="Pipeline not found") + + active_pipelines[pipeline_id]["status"] = "cancelled" + return {"status": "cancelled", "pipeline_id": pipeline_id} + + # =========================================== # Help API # =========================================== diff --git a/services/dashboard/templates/index.html b/services/dashboard/templates/index.html index c49683a..57053ff 100644 --- a/services/dashboard/templates/index.html +++ b/services/dashboard/templates/index.html @@ -102,6 +102,30 @@ Canada + + +
+ 📁 Project: + + + +
+ +
@@ -622,6 +658,10 @@

Discovered hosts with OS detection

+
+
+ +
+ +
+ +
@@ -1215,6 +1307,157 @@ + + +
+
+

+ 🔐 Credentials + ( total) +

+
+ +
+
+ + + + +
+ + +
+
+

+ 📝 Project Notes + ( notes) +

+
+ +
+
+ + + + +
@@ -1405,6 +1648,345 @@ + +
+
+

🚀 Automated Recon Pipeline

+ +
+
+ + +
+ +
+ +
+ + + + +
+
+ +
+ + +
+ + +
+

Active Pipelines

+
+ +
+
+
+ +
+ + +
+
+
+ + +
+
+

📁 Create New Project

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

+ 💡 All hosts, scans, credentials, and findings will be saved to this project. +

+
+
+ +
+ + +
+
+
+ + +
+
+
+

📁 Project Details

+ +
+ + +
+
+ + +
+
+

🔐 Add Credential

+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ + +
+
+

📝 Add Note

+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+
+
@@ -1787,6 +2369,58 @@ // Onboarding State showOnboarding: !localStorage.getItem('goosestrike-onboarded'), + // Project Management State + projects: [], + currentProjectId: '', + currentProject: null, + showProjectModal: false, + showProjectDetails: false, + newProject: { + name: '', + description: '', + target_network: '', + scope: [] + }, + + // Credentials Manager State + credentials: [], + showCredentialModal: false, + newCredential: { + username: '', + password: '', + hash: '', + hash_type: '', + domain: '', + host: '', + service: '', + source: '', + notes: '' + }, + + // Notes State + projectNotes: [], + showNoteModal: false, + newNote: { + title: '', + content: '', + category: 'general', + host: '' + }, + + // Exploit Suggestion State + exploitSuggestions: [], + exploitLoading: false, + + // Recon Pipeline State + showPipelineModal: false, + activePipelines: [], + newPipeline: { + target: '', + pipeline: 'standard', + include_vuln_scan: false, + include_web_enum: false + }, + phases: [ { id: 'recon', @@ -1886,6 +2520,8 @@ async init() { await this.checkStatus(); await this.checkRunningProcesses(); + await this.loadProjects(); // Load projects on init + await this.loadActivePipelines(); // Load active pipelines setInterval(() => this.checkStatus(), 30000); setInterval(() => this.checkRunningProcesses(), 5000); await this.loadProviders(); @@ -1967,6 +2603,330 @@ Select a phase above to begin, or use the quick actions in the sidebar!` } catch (e) { console.error('Failed to check processes:', e); } }, + // ============= PROJECT MANAGEMENT METHODS ============= + async loadProjects() { + try { + const response = await fetch('/api/projects'); + const data = await response.json(); + this.projects = data.projects || []; + this.currentProjectId = data.current_project_id || ''; + if (this.currentProjectId) { + await this.loadCurrentProject(); + } + } catch (e) { console.error('Failed to load projects:', e); } + }, + + async loadCurrentProject() { + if (!this.currentProjectId) { + this.currentProject = null; + this.networkHosts = []; + this.credentials = []; + this.projectNotes = []; + return; + } + try { + const response = await fetch(`/api/projects/${this.currentProjectId}`); + this.currentProject = await response.json(); + // Sync project data to local state + this.networkHosts = this.currentProject.hosts || []; + this.credentials = this.currentProject.credentials || []; + this.projectNotes = this.currentProject.notes || []; + // Refresh network map if visible + if (this.activeTab === 'network-map') { + this.refreshNetworkMap(); + } + } catch (e) { console.error('Failed to load current project:', e); } + }, + + async selectProject() { + if (this.currentProjectId) { + try { + await fetch(`/api/projects/${this.currentProjectId}/select`, { method: 'POST' }); + await this.loadCurrentProject(); + } catch (e) { console.error('Failed to select project:', e); } + } else { + this.currentProject = null; + // Load hosts from non-project storage + await this.refreshNetworkHosts(); + } + }, + + async createProject() { + if (!this.newProject.name.trim()) return; + try { + const response = await fetch('/api/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.newProject) + }); + const data = await response.json(); + if (data.status === 'success') { + this.projects.push(data.project); + this.currentProjectId = data.project.id; + await this.loadCurrentProject(); + this.showProjectModal = false; + this.newProject = { name: '', description: '', target_network: '', scope: [] }; + this.terminalHistory.push({ + type: 'success', + content: `📁 Project "${data.project.name}" created and selected.` + }); + } + } catch (e) { console.error('Failed to create project:', e); } + }, + + async deleteProject(projectId) { + if (!confirm('Delete this project and all its data?')) return; + try { + await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); + this.projects = this.projects.filter(p => p.id !== projectId); + if (this.currentProjectId === projectId) { + this.currentProjectId = ''; + this.currentProject = null; + } + } catch (e) { console.error('Failed to delete project:', e); } + }, + + // Credentials Management + async addCredential() { + if (!this.currentProjectId) { + alert('Please select a project first'); + return; + } + if (!this.newCredential.username.trim()) return; + try { + const response = await fetch(`/api/projects/${this.currentProjectId}/credentials`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.newCredential) + }); + const data = await response.json(); + if (data.status === 'success') { + this.credentials.push(data.credential); + this.showCredentialModal = false; + this.newCredential = { username: '', password: '', hash: '', hash_type: '', domain: '', host: '', service: '', source: '', notes: '' }; + } + } catch (e) { console.error('Failed to add credential:', e); } + }, + + async deleteCredential(credId) { + if (!this.currentProjectId) return; + try { + await fetch(`/api/projects/${this.currentProjectId}/credentials/${credId}`, { method: 'DELETE' }); + this.credentials = this.credentials.filter(c => c.id !== credId); + } catch (e) { console.error('Failed to delete credential:', e); } + }, + + // Notes Management + async addNote() { + if (!this.currentProjectId) { + alert('Please select a project first'); + return; + } + if (!this.newNote.title.trim() || !this.newNote.content.trim()) return; + try { + const response = await fetch(`/api/projects/${this.currentProjectId}/notes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.newNote) + }); + const data = await response.json(); + if (data.status === 'success') { + this.projectNotes.push(data.note); + this.showNoteModal = false; + this.newNote = { title: '', content: '', category: 'general', host: '' }; + } + } catch (e) { console.error('Failed to add note:', e); } + }, + + async deleteNote(noteId) { + if (!this.currentProjectId) return; + try { + await fetch(`/api/projects/${this.currentProjectId}/notes/${noteId}`, { method: 'DELETE' }); + this.projectNotes = this.projectNotes.filter(n => n.id !== noteId); + } catch (e) { console.error('Failed to delete note:', e); } + }, + + // ============= EXPLOIT SUGGESTION METHODS ============= + async getExploitSuggestions(host) { + if (!host) return; + this.exploitLoading = true; + this.exploitSuggestions = []; + + try { + const response = await fetch('/api/exploits/suggest', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ host_ip: host.ip }) + }); + const data = await response.json(); + this.exploitSuggestions = data.suggestions || []; + + if (this.exploitSuggestions.length === 0) { + this.terminalHistory.push({ + type: 'info', + content: `No known exploits found for ${host.ip}. Try running a version scan first: nmap -sV ${host.ip}` + }); + } else { + this.terminalHistory.push({ + type: 'success', + content: `Found ${this.exploitSuggestions.length} potential exploits for ${host.ip}` + }); + } + } catch (e) { + console.error('Failed to get exploit suggestions:', e); + this.terminalHistory.push({ + type: 'error', + content: `Failed to analyze host: ${e.message}` + }); + } + this.exploitLoading = false; + }, + + async getAllExploitSuggestions() { + if (!this.currentProjectId) { + alert('Please select a project first'); + return; + } + this.exploitLoading = true; + + try { + const response = await fetch('/api/exploits/suggest', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ all_hosts: true }) + }); + const data = await response.json(); + this.exploitSuggestions = data.suggestions || []; + + this.terminalHistory.push({ + type: 'success', + content: `Analyzed ${data.hosts_analyzed} hosts. Found ${data.total} potential exploits.` + }); + } catch (e) { + console.error('Failed to get exploit suggestions:', e); + } + this.exploitLoading = false; + }, + + runMsfModule(module, targetIp) { + const cmd = `msfconsole -q -x "use ${module}; set RHOSTS ${targetIp}; check"`; + this.terminalInput = cmd; + this.activeTab = 'terminal'; + this.terminalHistory.push({ + type: 'info', + content: `Loaded MSF command: ${cmd}` + }); + }, + + runManualCheck(checkCmd, targetIp) { + const cmd = checkCmd.replace(/TARGET/g, targetIp); + this.terminalInput = cmd; + this.activeTab = 'terminal'; + }, + + async searchExploits(query) { + try { + const response = await fetch(`/api/exploits/search/${encodeURIComponent(query)}`); + const data = await response.json(); + return data.results || []; + } catch (e) { + console.error('Failed to search exploits:', e); + return []; + } + }, + // ============= END EXPLOIT SUGGESTIONS ============= + + // ============= RECON PIPELINE METHODS ============= + async startPipeline() { + if (!this.newPipeline.target) return; + + try { + const response = await fetch('/api/recon/pipeline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(this.newPipeline) + }); + const data = await response.json(); + + if (data.status === 'started') { + this.terminalHistory.push({ + type: 'success', + content: `🚀 Pipeline started: ${data.message}\nPipeline ID: ${data.pipeline_id}\nEstimated time: ~${data.estimated_time} minutes` + }); + + // Start polling for pipeline status + this.pollPipelineStatus(data.pipeline_id); + + // Reset form + this.newPipeline.target = ''; + this.showPipelineModal = false; + } + } catch (e) { + console.error('Failed to start pipeline:', e); + this.terminalHistory.push({ + type: 'error', + content: `Failed to start pipeline: ${e.message}` + }); + } + }, + + async pollPipelineStatus(pipelineId) { + const poll = async () => { + try { + const response = await fetch(`/api/recon/pipeline/${pipelineId}`); + const data = await response.json(); + + // Update active pipelines list + const existing = this.activePipelines.find(p => p.id === pipelineId); + if (existing) { + Object.assign(existing, data); + } else { + this.activePipelines.push(data); + } + + // Update hosts from pipeline + if (data.hosts_discovered > 0) { + await this.refreshNetworkHosts(); + } + + // Continue polling if not complete + if (data.status === 'running') { + setTimeout(poll, 5000); + } else if (data.status === 'completed') { + this.terminalHistory.push({ + type: 'success', + content: `✅ Pipeline completed!\nTarget: ${data.target}\nHosts discovered: ${data.hosts_discovered}\nStages completed: ${data.total_stages}` + }); + await this.refreshNetworkHosts(); + } + } catch (e) { + console.error('Failed to poll pipeline:', e); + } + }; + + poll(); + }, + + async loadActivePipelines() { + try { + const response = await fetch('/api/recon/pipelines'); + const data = await response.json(); + this.activePipelines = data.pipelines || []; + + // Resume polling for running pipelines + for (const pipeline of this.activePipelines) { + if (pipeline.status === 'running') { + this.pollPipelineStatus(pipeline.id); + } + } + } catch (e) { + console.error('Failed to load pipelines:', e); + } + }, + // ============= END RECON PIPELINE ============= + + // ============= END PROJECT MANAGEMENT ============= + async loadProviders() { try { const response = await fetch('/api/providers'); @@ -2574,6 +3534,11 @@ Select a phase above to begin, or use the quick actions in the sidebar!` this.networkMapLoading = false; }, + async refreshNetworkHosts() { + // Alias for refreshNetworkMap - loads hosts from API + await this.refreshNetworkMap(); + }, + scanHost(ip) { this.scanModal = { tool: 'nmap', target: ip, scanType: 'full', types: ['quick', 'full', 'vuln'] }; this.scanModalOpen = true;