diff --git a/docker-compose.yml b/docker-compose.yml index 42fbd6b..d6be99d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: - LLM_ROUTER_URL=http://strikepackage-llm-router:8000 - KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002 - DEFAULT_LLM_PROVIDER=${DEFAULT_LLM_PROVIDER:-ollama} - - DEFAULT_LLM_MODEL=${DEFAULT_LLM_MODEL:-llama3.2} + - DEFAULT_LLM_MODEL=${DEFAULT_LLM_MODEL:-llama3.1:latest} depends_on: - llm-router - kali-executor @@ -50,8 +50,8 @@ services: - KALI_CONTAINER_NAME=strikepackage-kali volumes: - /var/run/docker.sock:/var/run/docker.sock - depends_on: - - kali + # depends_on: + # - kali # Temporarily disabled due to Kali mirror SSL issues networks: - strikepackage-net restart: unless-stopped @@ -67,12 +67,16 @@ services: environment: - OPENAI_API_KEY=${OPENAI_API_KEY:-} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - # Multi-endpoint support: comma-separated URLs - - OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://192.168.1.50:11434} + # Local Ollama endpoint (use host.docker.internal to reach host machine from container) + - OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://host.docker.internal:11434} + # Network Ollama endpoints (comma-separated for multiple) + - OLLAMA_NETWORK_URLS=${OLLAMA_NETWORK_URLS:-http://192.168.1.50:11434} # Legacy single endpoint (fallback) - - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://192.168.1.50:11434} + - OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434} # Load balancing: round-robin, random, failover - LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin} + extra_hosts: + - "host.docker.internal:host-gateway" networks: - strikepackage-net restart: unless-stopped diff --git a/services/dashboard/app/main.py b/services/dashboard/app/main.py index bb3a522..8cb2da5 100644 --- a/services/dashboard/app/main.py +++ b/services/dashboard/app/main.py @@ -43,7 +43,7 @@ class ChatMessage(BaseModel): message: str session_id: Optional[str] = None provider: str = "ollama" - model: str = "llama3.2" + model: str = "llama3.1:latest" context: Optional[str] = None @@ -51,14 +51,14 @@ class PhaseChatMessage(BaseModel): message: str phase: str provider: str = "ollama" - model: str = "llama3.2" + model: str = "llama3.1:latest" findings: List[Dict[str, Any]] = Field(default_factory=list) class AttackChainRequest(BaseModel): findings: List[Dict[str, Any]] provider: str = "ollama" - model: str = "llama3.2" + model: str = "llama3.1:latest" class CommandRequest(BaseModel): @@ -79,6 +79,18 @@ class NetworkScanRequest(BaseModel): scan_type: str = "os" # ping, quick, os, full +class TerminalScanRecord(BaseModel): + tool: str + target: str + command: str + output: str + source: str = "terminal" + + +# Local storage for terminal scans +terminal_scans: List[Dict[str, Any]] = [] + + @app.get("/health") async def health_check(): """Health check endpoint""" @@ -312,20 +324,73 @@ async def get_scan_result(scan_id: str): @app.get("/api/scans") async def list_scans(): - """List all scans""" + """List all scans (from hackgpt-api and terminal)""" + all_scans = list(terminal_scans) # Start with terminal scans try: async with httpx.AsyncClient() as client: response = await client.get(f"{HACKGPT_API_URL}/scans", timeout=10.0) if response.status_code == 200: - return response.json() - raise HTTPException(status_code=response.status_code, detail=response.text) + api_scans = response.json() + all_scans.extend(api_scans) except httpx.ConnectError: - raise HTTPException(status_code=503, detail="HackGPT API not available") + pass # Return terminal scans even if API is down + return all_scans + + +@app.post("/api/scans/terminal") +async def record_terminal_scan(scan: TerminalScanRecord): + """Record a scan run from the terminal""" + import uuid + from datetime import datetime + + scan_id = str(uuid.uuid4()) + + # Parse target from command if not provided + target = scan.target + if target == "unknown" and scan.command: + parts = scan.command.split() + # Try to find target (usually last non-flag argument) + for part in reversed(parts): + if not part.startswith('-') and '/' not in part: + target = part + break + + scan_record = { + "scan_id": scan_id, + "tool": scan.tool, + "target": target, + "scan_type": "terminal", + "command": scan.command, + "status": "completed", + "started_at": datetime.utcnow().isoformat(), + "completed_at": datetime.utcnow().isoformat(), + "result": { + "stdout": scan.output, + "stderr": "", + "exit_code": 0 + }, + "source": scan.source, + "parsed": None + } + + # Parse the output for common tools + if scan.tool == "nmap": + scan_record["parsed"] = parse_nmap_normal(scan.output) + + terminal_scans.append(scan_record) + + # Keep only last 100 terminal scans + if len(terminal_scans) > 100: + terminal_scans.pop(0) + + return {"status": "recorded", "scan_id": scan_id} @app.delete("/api/scans/clear") async def clear_scans(): """Clear all scan history""" + global terminal_scans + terminal_scans = [] # Clear local terminal scans try: async with httpx.AsyncClient() as client: response = await client.delete(f"{HACKGPT_API_URL}/scans/clear", timeout=10.0) @@ -766,6 +831,301 @@ async def get_network_hosts(): return {"hosts": network_hosts} +class HostData(BaseModel): + ip: str + hostname: Optional[str] = None + mac: Optional[str] = None + vendor: Optional[str] = None + os: Optional[str] = None + os_accuracy: Optional[int] = None + device_type: Optional[str] = None + ports: List[Dict[str, Any]] = Field(default_factory=list) + status: str = "up" + source: str = "terminal" + + +class NmapResultData(BaseModel): + """Accept raw nmap output for parsing""" + output: str + source: str = "terminal" + + +@app.post("/api/network/hosts") +async def add_network_host(host: HostData): + """Add a single host to the network map (from terminal scans)""" + global network_hosts + + host_dict = host.dict() + existing = next((h for h in network_hosts if h["ip"] == host.ip), None) + if existing: + existing.update(host_dict) + else: + network_hosts.append(host_dict) + + return {"status": "added", "host": host_dict} + + +@app.post("/api/network/hosts/bulk") +async def add_network_hosts_bulk(hosts: List[HostData]): + """Add multiple hosts to the network map""" + global network_hosts + + added = 0 + updated = 0 + for host in hosts: + host_dict = host.dict() + existing = next((h for h in network_hosts if h["ip"] == host.ip), None) + if existing: + existing.update(host_dict) + updated += 1 + else: + network_hosts.append(host_dict) + added += 1 + + return {"status": "success", "added": added, "updated": updated, "total": len(network_hosts)} + + +@app.post("/api/network/nmap-results") +async def add_nmap_results(data: NmapResultData): + """Parse and add nmap output to network map (supports both XML and grepable formats)""" + global network_hosts + + hosts = parse_nmap_xml(data.output) + + # If XML parsing didn't work, try parsing grepable/normal output + if not hosts: + hosts = parse_nmap_normal(data.output) + + added = 0 + updated = 0 + for host in hosts: + host["source"] = data.source + existing = next((h for h in network_hosts if h["ip"] == host["ip"]), None) + if existing: + existing.update(host) + updated += 1 + else: + network_hosts.append(host) + added += 1 + + return {"status": "success", "added": added, "updated": updated, "hosts_parsed": len(hosts), "total": len(network_hosts)} + + +def parse_nmap_normal(output: str) -> List[Dict[str, Any]]: + """Parse normal nmap output (non-XML)""" + import re + hosts = [] + current_host = None + + for line in output.split('\n'): + # Match "Nmap scan report for hostname (ip)" or "Nmap scan report for ip" + report_match = re.match(r'Nmap scan report for (?:([^\s(]+)\s+\()?([0-9.]+)\)?', line) + if report_match: + if current_host: + # Determine OS type before saving + current_host["os_type"] = determine_os_type( + current_host.get("os", ""), + current_host.get("ports", []), + current_host.get("mac", ""), + current_host.get("vendor", "") + ) + hosts.append(current_host) + hostname = report_match.group(1) or "" + ip = report_match.group(2) + current_host = { + "ip": ip, + "hostname": hostname, + "status": "up", + "ports": [], + "os": "", + "os_type": "", + "device_type": "" + } + continue + + # Match "Host is up" + if current_host and "Host is up" in line: + current_host["status"] = "up" + continue + + # Match port lines like "80/tcp open http Apache httpd" + port_match = re.match(r'(\d+)/(tcp|udp)\s+(\w+)\s+(\S+)(?:\s+(.*))?', line) + if port_match and current_host: + port_info = { + "port": int(port_match.group(1)), + "protocol": port_match.group(2), + "state": port_match.group(3), + "service": port_match.group(4), + "version": (port_match.group(5) or "").strip() + } + current_host["ports"].append(port_info) + continue + + # Match MAC address + mac_match = re.match(r'MAC Address:\s+([0-9A-F:]+)\s*(?:\((.+)\))?', line, re.IGNORECASE) + if mac_match and current_host: + current_host["mac"] = mac_match.group(1) + current_host["vendor"] = mac_match.group(2) or "" + continue + + # Match OS detection - multiple patterns + os_match = re.match(r'(?:OS details|Running|OS):\s+(.+)', line) + if os_match and current_host: + os_info = os_match.group(1).strip() + if os_info and not current_host["os"]: + current_host["os"] = os_info + elif os_info: + current_host["os"] += "; " + os_info + continue + + # Match smb-os-discovery OS line + smb_os_match = re.match(r'\|\s+OS:\s+(.+)', line) + if smb_os_match and current_host: + os_info = smb_os_match.group(1).strip() + if not current_host["os"]: + current_host["os"] = os_info + elif os_info not in current_host["os"]: + current_host["os"] += "; " + os_info + continue + + # Match device type + device_match = re.match(r'Device type:\s+(.+)', line) + if device_match and current_host: + current_host["device_type"] = device_match.group(1) + continue + + # Don't forget the last host + if current_host: + current_host["os_type"] = determine_os_type( + current_host.get("os", ""), + current_host.get("ports", []), + current_host.get("mac", ""), + current_host.get("vendor", "") + ) + hosts.append(current_host) + + return hosts + + +def determine_os_type(os_string: str, ports: List[Dict], mac: str = "", vendor: str = "") -> str: + """Determine OS type from OS string, open ports, MAC address, and vendor""" + os_lower = os_string.lower() + vendor_lower = vendor.lower() if vendor else "" + + # Check OS string for keywords + if any(kw in os_lower for kw in ['windows', 'microsoft', 'win32', 'win64']): + return 'windows' + if any(kw in os_lower for kw in ['linux', 'ubuntu', 'debian', 'centos', 'fedora', 'redhat', 'kali']): + return 'linux' + if any(kw in os_lower for kw in ['mac os', 'macos', 'darwin', 'apple']): + return 'macos' + if any(kw in os_lower for kw in ['cisco', 'router', 'switch', 'juniper', 'mikrotik']): + return 'router' + if any(kw in os_lower for kw in ['unix', 'freebsd', 'openbsd', 'solaris']): + return 'linux' # Close enough for icon purposes + + # Check MAC address OUI for manufacturer hints + mac_vendor = get_mac_vendor_hint(mac, vendor) + if mac_vendor: + return mac_vendor + + # Infer from ports if OS string didn't help + port_nums = [p.get('port') for p in ports if p.get('state') == 'open'] + services = [p.get('service', '').lower() for p in ports] + versions = [p.get('version', '').lower() for p in ports] + + # Windows indicators + windows_ports = {135, 139, 445, 3389, 5985, 5986} + if windows_ports & set(port_nums): + # Check if it's actually Samba on Linux + if any('samba' in v for v in versions): + return 'linux' + return 'windows' + + # Check service versions for OS hints + for version in versions: + if 'windows' in version or 'microsoft' in version: + return 'windows' + if 'ubuntu' in version or 'debian' in version or 'linux' in version: + return 'linux' + + # Linux indicators + if 22 in port_nums and any(s in services for s in ['ssh', 'openssh']): + return 'linux' + + # Router/network device indicators + if any(s in services for s in ['telnet', 'snmp']) and 80 in port_nums: + return 'router' + + return 'unknown' + + +def get_mac_vendor_hint(mac: str, vendor: str = "") -> Optional[str]: + """Get OS type hint from MAC address OUI or vendor string""" + if not mac and not vendor: + return None + + vendor_lower = vendor.lower() if vendor else "" + + # Check vendor string first (nmap often provides this) + # Apple devices + if any(kw in vendor_lower for kw in ['apple', 'iphone', 'ipad', 'macbook']): + return 'macos' + + # Network equipment + if any(kw in vendor_lower for kw in ['cisco', 'juniper', 'netgear', 'linksys', 'tp-link', 'ubiquiti', 'mikrotik', 'd-link', 'asus router', 'arris', 'netcomm']): + return 'router' + + # VM/Hypervisor (likely running any OS, but probably server/linux) + if any(kw in vendor_lower for kw in ['vmware', 'virtualbox', 'hyper-v', 'microsoft hyper', 'xen', 'qemu', 'kvm']): + return 'server' + + # Raspberry Pi + if 'raspberry' in vendor_lower: + return 'linux' + + # Known PC/laptop manufacturers (could be Windows or Linux) + pc_vendors = ['dell', 'hewlett', 'lenovo', 'asus', 'acer', 'msi', 'gigabyte', 'intel', 'realtek', 'broadcom', 'qualcomm', 'mediatek'] + if any(kw in vendor_lower for kw in pc_vendors): + # Most likely Windows for consumer PCs, but we can't be certain + # Return None to let other detection methods decide + return None + + # MAC OUI lookup for common prefixes + if mac: + mac_upper = mac.upper().replace(':', '').replace('-', '')[:6] + + # VMware + if mac_upper.startswith(('005056', '000C29', '000569')): + return 'server' + + # Microsoft Hyper-V + if mac_upper.startswith('00155D'): + return 'server' + + # Apple + if mac_upper.startswith(('A4B197', '3C0754', '009027', 'ACDE48', 'F0B479', '70CD60', '00A040', '000A27', '000393', '001CB3', '001D4F', '001E52', '001F5B', '001FF3', '0021E9', '002241', '002312', '002332', '002436', '00254B', '0025BC', '002608', '00264A', '0026B0', '0026BB')): + return 'macos' + + # Raspberry Pi Foundation + if mac_upper.startswith(('B827EB', 'DCA632', 'E45F01', 'DC2632')): + return 'linux' + + # Cisco + if mac_upper.startswith(('001121', '00166D', '001819', '001832', '00186B', '00187D', '0018B9', '0018F3', '00195E', '001A2F', '001A6C', '001A6D', '001B2A', '001BD4', '001C01', '001C0E', '001CE6', '001E13', '001E49', '001EBD', '001F27', '001F6C', '001F9D', '00212F', '002155', '0021A0', '0021BE', '0021D7', '002216', '00223A', '002255', '00226B', '002275', '0023AB', '0023AC', '0023BE', '0023EA', '00240A', '002414', '002436', '00248C', '0024F7', '00250B', '002583', '0025B4', '0026CB', '0027DC', '002841', '0029C2', '002A10', '002A6A', '002CC8', '0030A3', '003080', '0030B6', '00400B', '004096', '005000', '005014', '00501E', '00503E', '005050', '00505F', '005073', '0050A2', '0050D1', '0050E2', '0050F0', '006009', '00602F', '006047', '006052', '00606D', '00607B', '006083', '00609E', '0060B0', '00908F', '0090A6', '009092')): + return 'router' + + return None + + +@app.delete("/api/network/hosts") +async def clear_network_hosts(): + """Clear all network hosts""" + global network_hosts + network_hosts = [] + return {"status": "cleared"} + + 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/templates/index.html b/services/dashboard/templates/index.html index 0913c79..92f6da2 100644 --- a/services/dashboard/templates/index.html +++ b/services/dashboard/templates/index.html @@ -62,7 +62,7 @@ .network-link.active { stroke: #dc2626; stroke-width: 3; } .node-label { font-size: 11px; fill: #a3a3a3; text-anchor: middle; } .node-ip { font-size: 10px; fill: #666; text-anchor: middle; font-family: monospace; } - .device-icon { font-size: 24px; } + .device-icon { font-size: 24px; font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif; line-height: 1; } @@ -537,8 +537,8 @@ -
- +
+
@@ -799,6 +799,26 @@
+ + +