mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
V2.1: Network Map integration, Scan History, OS detection improvements
- Added nmap wrapper to auto-send scan results to Dashboard - Network Map now displays hosts from terminal scans - Scan History tab shows all scans (GUI and terminal) - Load previous scans to Network Map feature - Improved OS detection from nmap output (parses OS details, smb-os-discovery) - Added determine_os_type() with OUI/MAC vendor lookup - Static network map layout (no more jumpy D3 force simulation) - Fixed docker-compose for Ollama connectivity (host.docker.internal) - Added test_services.sh for comprehensive testing
This commit is contained in:
@@ -30,7 +30,7 @@ services:
|
|||||||
- LLM_ROUTER_URL=http://strikepackage-llm-router:8000
|
- LLM_ROUTER_URL=http://strikepackage-llm-router:8000
|
||||||
- KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
|
- KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
|
||||||
- DEFAULT_LLM_PROVIDER=${DEFAULT_LLM_PROVIDER:-ollama}
|
- 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:
|
depends_on:
|
||||||
- llm-router
|
- llm-router
|
||||||
- kali-executor
|
- kali-executor
|
||||||
@@ -50,8 +50,8 @@ services:
|
|||||||
- KALI_CONTAINER_NAME=strikepackage-kali
|
- KALI_CONTAINER_NAME=strikepackage-kali
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
depends_on:
|
# depends_on:
|
||||||
- kali
|
# - kali # Temporarily disabled due to Kali mirror SSL issues
|
||||||
networks:
|
networks:
|
||||||
- strikepackage-net
|
- strikepackage-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -67,12 +67,16 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
# Multi-endpoint support: comma-separated URLs
|
# Local Ollama endpoint (use host.docker.internal to reach host machine from container)
|
||||||
- OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://192.168.1.50:11434}
|
- 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)
|
# 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 balancing: round-robin, random, failover
|
||||||
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin}
|
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
networks:
|
networks:
|
||||||
- strikepackage-net
|
- strikepackage-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ChatMessage(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
session_id: Optional[str] = None
|
session_id: Optional[str] = None
|
||||||
provider: str = "ollama"
|
provider: str = "ollama"
|
||||||
model: str = "llama3.2"
|
model: str = "llama3.1:latest"
|
||||||
context: Optional[str] = None
|
context: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -51,14 +51,14 @@ class PhaseChatMessage(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
phase: str
|
phase: str
|
||||||
provider: str = "ollama"
|
provider: str = "ollama"
|
||||||
model: str = "llama3.2"
|
model: str = "llama3.1:latest"
|
||||||
findings: List[Dict[str, Any]] = Field(default_factory=list)
|
findings: List[Dict[str, Any]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class AttackChainRequest(BaseModel):
|
class AttackChainRequest(BaseModel):
|
||||||
findings: List[Dict[str, Any]]
|
findings: List[Dict[str, Any]]
|
||||||
provider: str = "ollama"
|
provider: str = "ollama"
|
||||||
model: str = "llama3.2"
|
model: str = "llama3.1:latest"
|
||||||
|
|
||||||
|
|
||||||
class CommandRequest(BaseModel):
|
class CommandRequest(BaseModel):
|
||||||
@@ -79,6 +79,18 @@ class NetworkScanRequest(BaseModel):
|
|||||||
scan_type: str = "os" # ping, quick, os, full
|
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")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
@@ -312,20 +324,73 @@ async def get_scan_result(scan_id: str):
|
|||||||
|
|
||||||
@app.get("/api/scans")
|
@app.get("/api/scans")
|
||||||
async def list_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:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.get(f"{HACKGPT_API_URL}/scans", timeout=10.0)
|
response = await client.get(f"{HACKGPT_API_URL}/scans", timeout=10.0)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
api_scans = response.json()
|
||||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
all_scans.extend(api_scans)
|
||||||
except httpx.ConnectError:
|
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")
|
@app.delete("/api/scans/clear")
|
||||||
async def clear_scans():
|
async def clear_scans():
|
||||||
"""Clear all scan history"""
|
"""Clear all scan history"""
|
||||||
|
global terminal_scans
|
||||||
|
terminal_scans = [] # Clear local terminal scans
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.delete(f"{HACKGPT_API_URL}/scans/clear", timeout=10.0)
|
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}
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
.network-link.active { stroke: #dc2626; stroke-width: 3; }
|
.network-link.active { stroke: #dc2626; stroke-width: 3; }
|
||||||
.node-label { font-size: 11px; fill: #a3a3a3; text-anchor: middle; }
|
.node-label { font-size: 11px; fill: #a3a3a3; text-anchor: middle; }
|
||||||
.node-ip { font-size: 10px; fill: #666; text-anchor: middle; font-family: monospace; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-sp-black text-sp-white">
|
<body class="bg-sp-black text-sp-white">
|
||||||
@@ -537,8 +537,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Network Map SVG -->
|
<!-- Network Map SVG -->
|
||||||
<div x-show="networkHosts.length > 0 && !networkMapLoading" class="flex-1 network-map-container relative">
|
<div x-show="networkHosts.length > 0 && !networkMapLoading" class="flex-1 network-map-container relative" style="min-height: 500px; height: calc(100vh - 280px);">
|
||||||
<svg id="networkMapSvg" class="w-full h-full"></svg>
|
<svg id="networkMapSvg" style="width: 100%; height: 100%; display: block;"></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Host Details Panel -->
|
<!-- Host Details Panel -->
|
||||||
@@ -799,6 +799,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Show hosts if parsed has hosts (nmap scans) -->
|
||||||
|
<template x-if="selectedScan.parsed && (Array.isArray(selectedScan.parsed) || selectedScan.parsed.hosts)">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm text-sp-white-muted mb-2">Discovered Hosts (<span x-text="(Array.isArray(selectedScan.parsed) ? selectedScan.parsed : selectedScan.parsed.hosts || []).length"></span>)</h4>
|
||||||
|
<div class="space-y-2 max-h-48 overflow-y-auto mb-3">
|
||||||
|
<template x-for="host in (Array.isArray(selectedScan.parsed) ? selectedScan.parsed : selectedScan.parsed.hosts || [])" :key="host.ip">
|
||||||
|
<div class="flex items-center gap-3 bg-sp-black p-2 rounded text-sm">
|
||||||
|
<span class="text-sp-red font-mono" x-text="host.ip"></span>
|
||||||
|
<span class="text-sp-white-muted" x-text="host.hostname || ''"></span>
|
||||||
|
<span class="text-green-400 text-xs" x-text="host.ports ? host.ports.filter(p => p.state === 'open').length + ' open ports' : ''"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button @click="loadScanToNetworkMap(selectedScan)"
|
||||||
|
class="w-full bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center justify-center gap-2">
|
||||||
|
🗺️ Load to Network Map
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template x-if="selectedScan.result && selectedScan.result.stdout">
|
<template x-if="selectedScan.result && selectedScan.result.stdout">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm text-sp-white-muted mb-2">Raw Output</h4>
|
<h4 class="text-sm text-sp-white-muted mb-2">Raw Output</h4>
|
||||||
@@ -1264,6 +1284,49 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
|
|||||||
|
|
||||||
viewScanDetails(scan) { this.selectedScan = scan; this.detailsModalOpen = true; },
|
viewScanDetails(scan) { this.selectedScan = scan; this.detailsModalOpen = true; },
|
||||||
|
|
||||||
|
loadScanToNetworkMap(scan) {
|
||||||
|
// Get hosts from the scan's parsed data
|
||||||
|
let hosts = [];
|
||||||
|
if (Array.isArray(scan.parsed)) {
|
||||||
|
hosts = scan.parsed;
|
||||||
|
} else if (scan.parsed && scan.parsed.hosts) {
|
||||||
|
hosts = scan.parsed.hosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hosts.length === 0) {
|
||||||
|
alert('No hosts found in this scan');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge hosts into networkHosts (update existing, add new)
|
||||||
|
hosts.forEach(host => {
|
||||||
|
const existing = this.networkHosts.find(h => h.ip === host.ip);
|
||||||
|
if (existing) {
|
||||||
|
// Update existing host
|
||||||
|
Object.assign(existing, host);
|
||||||
|
} else {
|
||||||
|
// Add new host
|
||||||
|
this.networkHosts.push({
|
||||||
|
ip: host.ip,
|
||||||
|
hostname: host.hostname || '',
|
||||||
|
os_type: host.os_type || host.os || '',
|
||||||
|
os_details: host.os_details || '',
|
||||||
|
ports: host.ports || [],
|
||||||
|
mac: host.mac || '',
|
||||||
|
vendor: host.vendor || '',
|
||||||
|
source: 'scan-history'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal and switch to network map
|
||||||
|
this.detailsModalOpen = false;
|
||||||
|
this.activeTab = 'network-map';
|
||||||
|
|
||||||
|
// Re-render network map
|
||||||
|
this.$nextTick(() => this.renderNetworkMap());
|
||||||
|
},
|
||||||
|
|
||||||
askAboutTool(tool) {
|
askAboutTool(tool) {
|
||||||
this.activeTab = 'phase';
|
this.activeTab = 'phase';
|
||||||
this.userInput = `Explain how to use ${tool.name} for ${this.getCurrentPhase().name.toLowerCase()}. Include common options and example commands.`;
|
this.userInput = `Explain how to use ${tool.name} for ${this.getCurrentPhase().name.toLowerCase()}. Include common options and example commands.`;
|
||||||
@@ -1569,117 +1632,114 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
|
|||||||
|
|
||||||
if (this.networkHosts.length === 0) return;
|
if (this.networkHosts.length === 0) return;
|
||||||
|
|
||||||
const container = document.getElementById('networkMapSvg').parentElement;
|
const container = document.getElementById('networkMapSvg');
|
||||||
const width = container.clientWidth;
|
if (!container) return;
|
||||||
const height = container.clientHeight;
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const width = rect.width || 800;
|
||||||
|
const height = rect.height || 500;
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
|
||||||
|
// Set explicit width/height on SVG
|
||||||
|
svg.attr('width', width)
|
||||||
|
.attr('height', height);
|
||||||
|
|
||||||
// Create a gateway node (center)
|
// Create a gateway node (center)
|
||||||
const gatewayIp = this.networkHosts[0]?.ip.split('.').slice(0, 3).join('.') + '.1';
|
const gatewayIp = this.networkHosts[0]?.ip.split('.').slice(0, 3).join('.') + '.1';
|
||||||
const nodes = [
|
|
||||||
{ id: 'gateway', ip: gatewayIp, os_type: 'router', hostname: 'Gateway', isGateway: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add host nodes
|
// Calculate static positions - gateway in center, hosts in circle
|
||||||
this.networkHosts.forEach(host => {
|
const hostCount = this.networkHosts.length;
|
||||||
nodes.push({
|
const radius = Math.min(width, height) * 0.35;
|
||||||
id: host.ip,
|
|
||||||
ip: host.ip,
|
// Draw links first (behind nodes)
|
||||||
os_type: host.os_type,
|
const linkGroup = svg.append('g');
|
||||||
hostname: host.hostname,
|
this.networkHosts.forEach((host, i) => {
|
||||||
ports: host.ports
|
const angle = (2 * Math.PI * i) / hostCount - Math.PI / 2;
|
||||||
});
|
const hostX = centerX + radius * Math.cos(angle);
|
||||||
|
const hostY = centerY + radius * Math.sin(angle);
|
||||||
|
|
||||||
|
linkGroup.append('line')
|
||||||
|
.attr('x1', centerX)
|
||||||
|
.attr('y1', centerY)
|
||||||
|
.attr('x2', hostX)
|
||||||
|
.attr('y2', hostY)
|
||||||
|
.attr('stroke', '#3a3a3a')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create links from each host to gateway
|
// Draw gateway node
|
||||||
const links = this.networkHosts.map(host => ({
|
const gatewayGroup = svg.append('g')
|
||||||
source: 'gateway',
|
.attr('transform', `translate(${centerX},${centerY})`)
|
||||||
target: host.ip
|
.style('cursor', 'pointer');
|
||||||
}));
|
|
||||||
|
|
||||||
// Force simulation
|
gatewayGroup.append('circle')
|
||||||
const simulation = d3.forceSimulation(nodes)
|
.attr('r', 40)
|
||||||
.force('link', d3.forceLink(links).id(d => d.id).distance(150))
|
.attr('fill', '#1f1f1f')
|
||||||
.force('charge', d3.forceManyBody().strength(-300))
|
.attr('stroke', '#dc2626')
|
||||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
.attr('stroke-width', 3);
|
||||||
.force('collision', d3.forceCollide().radius(60));
|
|
||||||
|
|
||||||
// Draw links
|
gatewayGroup.append('text')
|
||||||
const link = svg.append('g')
|
|
||||||
.selectAll('line')
|
|
||||||
.data(links)
|
|
||||||
.enter().append('line')
|
|
||||||
.attr('class', 'network-link')
|
|
||||||
.attr('stroke', '#3a3a3a')
|
|
||||||
.attr('stroke-width', 2);
|
|
||||||
|
|
||||||
// Draw nodes
|
|
||||||
const node = svg.append('g')
|
|
||||||
.selectAll('g')
|
|
||||||
.data(nodes)
|
|
||||||
.enter().append('g')
|
|
||||||
.attr('class', 'network-node')
|
|
||||||
.attr('data-ip', d => d.ip)
|
|
||||||
.style('cursor', 'pointer')
|
|
||||||
.on('click', (event, d) => {
|
|
||||||
if (!d.isGateway) {
|
|
||||||
const host = this.networkHosts.find(h => h.ip === d.ip);
|
|
||||||
if (host) this.selectHost(host);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Node circles
|
|
||||||
node.append('circle')
|
|
||||||
.attr('r', d => d.isGateway ? 35 : 30)
|
|
||||||
.attr('fill', d => d.isGateway ? '#1f1f1f' : '#2a2a2a')
|
|
||||||
.attr('stroke', d => d.isGateway ? '#dc2626' : '#3a3a3a')
|
|
||||||
.attr('stroke-width', 2);
|
|
||||||
|
|
||||||
// Device icons
|
|
||||||
node.append('text')
|
|
||||||
.attr('class', 'device-icon')
|
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('dy', '0.35em')
|
.attr('dy', '0.35em')
|
||||||
.text(d => this.getDeviceIcon(d.os_type));
|
.attr('font-size', '28px')
|
||||||
|
.text('📡');
|
||||||
|
|
||||||
// IP labels
|
gatewayGroup.append('text')
|
||||||
node.append('text')
|
|
||||||
.attr('class', 'node-ip')
|
.attr('class', 'node-ip')
|
||||||
.attr('dy', 50)
|
.attr('text-anchor', 'middle')
|
||||||
.text(d => d.ip);
|
.attr('dy', 60)
|
||||||
|
.attr('fill', '#666')
|
||||||
|
.attr('font-size', '10px')
|
||||||
|
.attr('font-family', 'monospace')
|
||||||
|
.text(gatewayIp);
|
||||||
|
|
||||||
// Hostname labels
|
gatewayGroup.append('text')
|
||||||
node.append('text')
|
|
||||||
.attr('class', 'node-label')
|
.attr('class', 'node-label')
|
||||||
.attr('dy', 65)
|
.attr('text-anchor', 'middle')
|
||||||
.text(d => d.hostname || '');
|
.attr('dy', 75)
|
||||||
|
.attr('fill', '#a3a3a3')
|
||||||
|
.attr('font-size', '11px')
|
||||||
|
.text('Gateway');
|
||||||
|
|
||||||
// Update positions on tick
|
// Draw host nodes
|
||||||
simulation.on('tick', () => {
|
this.networkHosts.forEach((host, i) => {
|
||||||
link
|
const angle = (2 * Math.PI * i) / hostCount - Math.PI / 2;
|
||||||
.attr('x1', d => d.source.x)
|
const hostX = centerX + radius * Math.cos(angle);
|
||||||
.attr('y1', d => d.source.y)
|
const hostY = centerY + radius * Math.sin(angle);
|
||||||
.attr('x2', d => d.target.x)
|
|
||||||
.attr('y2', d => d.target.y);
|
|
||||||
|
|
||||||
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
const hostGroup = svg.append('g')
|
||||||
|
.attr('transform', `translate(${hostX},${hostY})`)
|
||||||
|
.style('cursor', 'pointer')
|
||||||
|
.on('click', () => this.selectHost(host));
|
||||||
|
|
||||||
|
hostGroup.append('circle')
|
||||||
|
.attr('r', 35)
|
||||||
|
.attr('fill', '#2a2a2a')
|
||||||
|
.attr('stroke', '#3a3a3a')
|
||||||
|
.attr('stroke-width', 2);
|
||||||
|
|
||||||
|
hostGroup.append('text')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dy', '0.35em')
|
||||||
|
.attr('font-size', '24px')
|
||||||
|
.text(this.getDeviceIcon(host.os_type));
|
||||||
|
|
||||||
|
hostGroup.append('text')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dy', 55)
|
||||||
|
.attr('fill', '#dc2626')
|
||||||
|
.attr('font-size', '11px')
|
||||||
|
.attr('font-family', 'monospace')
|
||||||
|
.text(host.ip);
|
||||||
|
|
||||||
|
hostGroup.append('text')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('dy', 70)
|
||||||
|
.attr('fill', '#a3a3a3')
|
||||||
|
.attr('font-size', '10px')
|
||||||
|
.text(host.hostname ? host.hostname.substring(0, 20) : '');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drag behavior
|
|
||||||
node.call(d3.drag()
|
|
||||||
.on('start', (event, d) => {
|
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
})
|
|
||||||
.on('drag', (event, d) => {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
})
|
|
||||||
.on('end', (event, d) => {
|
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ KALI_EXECUTOR_URL = os.getenv("KALI_EXECUTOR_URL", "http://strikepackage-kali-ex
|
|||||||
|
|
||||||
# Default LLM Configuration (can be overridden via environment or API)
|
# Default LLM Configuration (can be overridden via environment or API)
|
||||||
DEFAULT_LLM_PROVIDER = os.getenv("DEFAULT_LLM_PROVIDER", "ollama")
|
DEFAULT_LLM_PROVIDER = os.getenv("DEFAULT_LLM_PROVIDER", "ollama")
|
||||||
DEFAULT_LLM_MODEL = os.getenv("DEFAULT_LLM_MODEL", "llama3.2")
|
DEFAULT_LLM_MODEL = os.getenv("DEFAULT_LLM_MODEL", "llama3.1:latest")
|
||||||
|
|
||||||
# In-memory storage (use Redis in production)
|
# In-memory storage (use Redis in production)
|
||||||
tasks: Dict[str, Any] = {}
|
tasks: Dict[str, Any] = {}
|
||||||
|
|||||||
@@ -3,35 +3,56 @@ FROM kalilinux/kali-rolling
|
|||||||
# Avoid prompts during package installation
|
# Avoid prompts during package installation
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Update and install ALL Kali tools
|
# Configure apt to use direct Kali mirrors (avoid CDN mirrors that get blocked by content filters)
|
||||||
# Using kali-linux-everything metapackage for complete tool suite
|
# The mirror.us.cdn-perfprod.com CDN is being blocked by SafeBrowse content filter
|
||||||
|
RUN echo 'deb http://kali.download/kali kali-rolling main non-free non-free-firmware contrib' > /etc/apt/sources.list && \
|
||||||
|
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
|
||||||
|
|
||||||
|
# Update and install essential security tools
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
kali-linux-everything \
|
nmap \
|
||||||
|
nikto \
|
||||||
|
sqlmap \
|
||||||
|
gobuster \
|
||||||
|
dirb \
|
||||||
|
wfuzz \
|
||||||
|
hydra \
|
||||||
|
john \
|
||||||
|
hashcat \
|
||||||
|
whois \
|
||||||
|
dnsutils \
|
||||||
|
net-tools \
|
||||||
|
iputils-ping \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
vim \
|
||||||
|
jq \
|
||||||
|
uuid-runtime \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install additional Python tools and utilities for command logging
|
# Install additional Python tools for command logging and scripting
|
||||||
RUN pip3 install --break-system-packages \
|
RUN pip3 install --break-system-packages \
|
||||||
requests \
|
requests \
|
||||||
beautifulsoup4 \
|
beautifulsoup4 \
|
||||||
shodan \
|
shodan \
|
||||||
censys
|
censys
|
||||||
|
|
||||||
# Install jq and uuid-runtime for command logging
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
jq \
|
|
||||||
uuid-runtime \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create workspace directory
|
# Create workspace directory
|
||||||
WORKDIR /workspace
|
WORKDIR /workspace
|
||||||
|
|
||||||
# Copy scripts
|
# Copy scripts and convert Windows line endings to Unix
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
COPY command_logger.sh /usr/local/bin/command_logger.sh
|
COPY command_logger.sh /usr/local/bin/command_logger.sh
|
||||||
COPY capture_wrapper.sh /usr/local/bin/capture
|
COPY capture_wrapper.sh /usr/local/bin/capture
|
||||||
RUN chmod +x /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture
|
COPY nmap_wrapper.sh /usr/local/bin/nmap
|
||||||
|
|
||||||
|
# Convert any Windows line endings (CRLF) to Unix (LF)
|
||||||
|
RUN sed -i 's/\r$//' /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture /usr/local/bin/nmap
|
||||||
|
RUN chmod +x /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture /usr/local/bin/nmap
|
||||||
|
|
||||||
# Create command history directory
|
# Create command history directory
|
||||||
RUN mkdir -p /workspace/.command_history
|
RUN mkdir -p /workspace/.command_history
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Output Capture Wrapper for Security Tools
|
# Output Capture Wrapper for Security Tools
|
||||||
# Wraps command execution to capture stdout/stderr and save results
|
# Wraps command execution to capture stdout/stderr and save results
|
||||||
|
# Automatically sends nmap results to dashboard network map
|
||||||
|
|
||||||
COMMAND_LOG_DIR="${COMMAND_LOG_DIR:-/workspace/.command_history}"
|
COMMAND_LOG_DIR="${COMMAND_LOG_DIR:-/workspace/.command_history}"
|
||||||
|
DASHBOARD_URL="${DASHBOARD_URL:-http://strikepackage-dashboard:8080}"
|
||||||
mkdir -p "$COMMAND_LOG_DIR"
|
mkdir -p "$COMMAND_LOG_DIR"
|
||||||
|
|
||||||
# Get command from arguments
|
# Get command from arguments
|
||||||
@@ -61,12 +63,35 @@ cat > "$output_file" << EOF
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Output results to terminal first
|
||||||
|
echo "$stdout_content"
|
||||||
|
[ -n "$stderr_content" ] && echo "$stderr_content" >&2
|
||||||
|
|
||||||
# Clean up temp files
|
# Clean up temp files
|
||||||
rm -f "$stdout_file" "$stderr_file"
|
rm -f "$stdout_file" "$stderr_file"
|
||||||
|
|
||||||
# Output results to terminal
|
# If this was an nmap command, send results to dashboard network map
|
||||||
cat "$stdout_file" 2>/dev/null || true
|
if [[ "$cmd_string" == nmap* ]] && [ $exit_code -eq 0 ]; then
|
||||||
cat "$stderr_file" >&2 2>/dev/null || true
|
echo "" >&2
|
||||||
|
echo "[StrikePackageGPT] Detected nmap scan, sending to Network Map..." >&2
|
||||||
|
|
||||||
|
# Send nmap output to dashboard for parsing
|
||||||
|
nmap_json=$(jq -n --arg output "$stdout_content" --arg source "terminal" \
|
||||||
|
'{output: $output, source: $source}')
|
||||||
|
|
||||||
|
response=$(curl -s -X POST "${DASHBOARD_URL}/api/network/nmap-results" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$nmap_json" 2>/dev/null || echo '{"error":"failed to connect"}')
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
added=$(echo "$response" | jq -r '.added // 0' 2>/dev/null)
|
||||||
|
updated=$(echo "$response" | jq -r '.updated // 0' 2>/dev/null)
|
||||||
|
total=$(echo "$response" | jq -r '.total // 0' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$added" != "null" ] && [ "$added" != "0" -o "$updated" != "0" ]; then
|
||||||
|
echo "[StrikePackageGPT] Network Map updated: $added added, $updated updated (total: $total hosts)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
echo "[StrikePackageGPT] Command captured: $cmd_id" >&2
|
echo "[StrikePackageGPT] Command captured: $cmd_id" >&2
|
||||||
|
|||||||
@@ -3,12 +3,48 @@
|
|||||||
# Enable command logging by default for all bash sessions
|
# Enable command logging by default for all bash sessions
|
||||||
echo 'source /usr/local/bin/command_logger.sh' >> /root/.bashrc
|
echo 'source /usr/local/bin/command_logger.sh' >> /root/.bashrc
|
||||||
echo 'export COMMAND_LOG_DIR=/workspace/.command_history' >> /root/.bashrc
|
echo 'export COMMAND_LOG_DIR=/workspace/.command_history' >> /root/.bashrc
|
||||||
|
echo 'export DASHBOARD_URL=http://strikepackage-dashboard:8080' >> /root/.bashrc
|
||||||
|
|
||||||
# Create convenience aliases for captured execution
|
# Create convenience aliases for captured execution
|
||||||
cat >> /root/.bashrc << 'ALIASES'
|
cat >> /root/.bashrc << 'ALIASES'
|
||||||
# Convenience alias to run commands with automatic capture
|
# Convenience alias to run commands with automatic capture
|
||||||
alias run='capture'
|
alias run='capture'
|
||||||
|
|
||||||
|
# Wrap nmap to automatically send results to network map
|
||||||
|
nmap_wrapper() {
|
||||||
|
local output
|
||||||
|
local exit_code
|
||||||
|
|
||||||
|
# Run nmap and capture output
|
||||||
|
output=$(/usr/bin/nmap "$@" 2>&1)
|
||||||
|
exit_code=$?
|
||||||
|
|
||||||
|
# Display output
|
||||||
|
echo "$output"
|
||||||
|
|
||||||
|
# If successful, send to dashboard network map
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "[StrikePackageGPT] Sending nmap results to Network Map..." >&2
|
||||||
|
|
||||||
|
# Send to dashboard
|
||||||
|
response=$(curl -s -X POST "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/nmap-results" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n --arg output "$output" --arg source "terminal" '{output: $output, source: $source}')" 2>/dev/null)
|
||||||
|
|
||||||
|
added=$(echo "$response" | jq -r '.added // 0' 2>/dev/null)
|
||||||
|
updated=$(echo "$response" | jq -r '.updated // 0' 2>/dev/null)
|
||||||
|
total=$(echo "$response" | jq -r '.total // 0' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$added" != "null" ] 2>/dev/null; then
|
||||||
|
echo "[StrikePackageGPT] Network Map: $added added, $updated updated (total: $total hosts)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return $exit_code
|
||||||
|
}
|
||||||
|
alias nmap='nmap_wrapper'
|
||||||
|
|
||||||
# Helper function to show recent commands
|
# Helper function to show recent commands
|
||||||
recent_commands() {
|
recent_commands() {
|
||||||
echo "Recent commands logged:"
|
echo "Recent commands logged:"
|
||||||
@@ -18,36 +54,42 @@ recent_commands() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
alias recent='recent_commands'
|
alias recent='recent_commands'
|
||||||
|
|
||||||
|
# Show network map hosts
|
||||||
|
show_hosts() {
|
||||||
|
echo "Network Map Hosts:"
|
||||||
|
curl -s "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/hosts" | jq -r '.hosts[] | "\(.ip)\t\(.hostname // "-")\t\(.os // "-")\tPorts: \(.ports | length)"' 2>/dev/null || echo "No hosts found"
|
||||||
|
}
|
||||||
|
alias hosts='show_hosts'
|
||||||
|
|
||||||
|
# Clear network map
|
||||||
|
clear_hosts() {
|
||||||
|
curl -s -X DELETE "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/hosts" | jq .
|
||||||
|
echo "Network map cleared"
|
||||||
|
}
|
||||||
ALIASES
|
ALIASES
|
||||||
|
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
echo " StrikePackageGPT - Kali Container"
|
echo " StrikePackageGPT - Kali Container"
|
||||||
echo " Security Tools Ready + Command Capture Enabled"
|
echo " Security Tools Ready + Network Map Integration"
|
||||||
echo "=================================================="
|
echo "=================================================="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Available tools:"
|
echo "Available tools:"
|
||||||
echo " - nmap, masscan (port scanning)"
|
echo " - nmap, masscan (port scanning)"
|
||||||
echo " - amass, theharvester (reconnaissance)"
|
|
||||||
echo " - nikto, gobuster (web testing)"
|
echo " - nikto, gobuster (web testing)"
|
||||||
echo " - sqlmap (SQL injection)"
|
echo " - sqlmap (SQL injection)"
|
||||||
echo " - hydra (brute force)"
|
echo " - hydra (brute force)"
|
||||||
echo " - metasploit (exploitation)"
|
|
||||||
echo " - searchsploit (exploit database)"
|
|
||||||
echo " - aircrack-ng, wifite (wireless)"
|
|
||||||
echo " - john, hashcat (password cracking)"
|
echo " - john, hashcat (password cracking)"
|
||||||
echo " - and 600+ more Kali tools"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔄 BIDIRECTIONAL CAPTURE ENABLED 🔄"
|
echo "🗺️ NETWORK MAP INTEGRATION ENABLED 🗺️"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands you run here will be captured and visible in:"
|
echo "nmap scans automatically appear in the Dashboard Network Map!"
|
||||||
echo " • Dashboard history"
|
|
||||||
echo " • API scan results"
|
|
||||||
echo " • Network visualization"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Usage:"
|
echo "Commands:"
|
||||||
echo " • Run commands normally: nmap -sV 192.168.1.1"
|
echo " • nmap -sV 192.168.1.1 - Scan and auto-add to map"
|
||||||
echo " • Use 'capture' prefix for explicit capture: capture nmap -sV 192.168.1.1"
|
echo " • hosts - Show network map hosts"
|
||||||
echo " • View recent: recent"
|
echo " • clear_hosts - Clear network map"
|
||||||
|
echo " • recent - Show recent commands"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Container is ready for security testing."
|
echo "Container is ready for security testing."
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
50
services/kali/nmap_wrapper.sh
Normal file
50
services/kali/nmap_wrapper.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# StrikePackageGPT nmap wrapper - sends scan results to Network Map and Scan History automatically
|
||||||
|
|
||||||
|
DASHBOARD_URL="${DASHBOARD_URL:-http://strikepackage-dashboard:8080}"
|
||||||
|
REAL_NMAP="/usr/bin/nmap"
|
||||||
|
|
||||||
|
# Capture the full command for logging
|
||||||
|
full_command="nmap $*"
|
||||||
|
|
||||||
|
# Determine target (last non-flag argument)
|
||||||
|
target="unknown"
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ ! "$arg" =~ ^- ]]; then
|
||||||
|
target="$arg"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create temp file for output
|
||||||
|
tmpfile=$(mktemp)
|
||||||
|
trap "rm -f $tmpfile" EXIT
|
||||||
|
|
||||||
|
# Run the actual nmap and capture output
|
||||||
|
"$REAL_NMAP" "$@" 2>&1 | tee "$tmpfile"
|
||||||
|
exit_code=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
# If successful, send to dashboard
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "[StrikePackageGPT] Sending results to Dashboard..." >&2
|
||||||
|
|
||||||
|
# Use jq with file input to avoid argument length limits
|
||||||
|
# Send to network map
|
||||||
|
jq -Rs --arg source "terminal" '{output: ., source: $source}' "$tmpfile" | \
|
||||||
|
curl -s -X POST "${DASHBOARD_URL}/api/network/nmap-results" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @- >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Send to scan history
|
||||||
|
response=$(jq -Rs --arg tool "nmap" --arg target "$target" --arg command "$full_command" \
|
||||||
|
'{tool: $tool, target: $target, command: $command, output: ., source: "terminal"}' "$tmpfile" | \
|
||||||
|
curl -s -X POST "${DASHBOARD_URL}/api/scans/terminal" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @- 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$response" ]; then
|
||||||
|
echo "[StrikePackageGPT] ✓ Results saved to Network Map and Scan History" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $exit_code
|
||||||
121
test_services.sh
Normal file
121
test_services.sh
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# StrikePackageGPT Service Test Script
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " StrikePackageGPT V2.1 Test Suite"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: Health Endpoints
|
||||||
|
echo "=== TEST 1: Health Endpoints ==="
|
||||||
|
echo "LLM Router:"
|
||||||
|
curl -s http://strikepackage-llm-router:8000/health | jq .
|
||||||
|
echo ""
|
||||||
|
echo "HackGPT API:"
|
||||||
|
curl -s http://strikepackage-hackgpt-api:8001/health | jq .
|
||||||
|
echo ""
|
||||||
|
echo "Kali Executor:"
|
||||||
|
curl -s http://strikepackage-kali-executor:8002/health | jq .
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: LLM Router
|
||||||
|
echo "=== TEST 2: LLM Router ==="
|
||||||
|
echo "Providers:"
|
||||||
|
curl -s http://strikepackage-llm-router:8000/providers | jq -r 'keys[]'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Chat Test (llama3.1:latest):"
|
||||||
|
cat > /tmp/chat.json << 'EOFCHAT'
|
||||||
|
{"provider":"ollama","model":"llama3.1:latest","messages":[{"role":"user","content":"Say hello in exactly 3 words"}]}
|
||||||
|
EOFCHAT
|
||||||
|
RESPONSE=$(curl -s -X POST http://strikepackage-llm-router:8000/chat -H "Content-Type: application/json" -d @/tmp/chat.json)
|
||||||
|
echo "$RESPONSE" | jq -r '.response // .content // .message // .' | head -c 200
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: HackGPT API
|
||||||
|
echo "=== TEST 3: HackGPT API ==="
|
||||||
|
echo "Tools available:"
|
||||||
|
curl -s http://strikepackage-hackgpt-api:8001/tools | jq -r '.[].name' 2>/dev/null || curl -s http://strikepackage-hackgpt-api:8001/tools | head -c 300
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Scans list:"
|
||||||
|
curl -s http://strikepackage-hackgpt-api:8001/scans | jq . 2>/dev/null || echo "[]"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: Kali Executor
|
||||||
|
echo "=== TEST 4: Kali Executor ==="
|
||||||
|
echo "Tools:"
|
||||||
|
curl -s http://strikepackage-kali-executor:8002/tools | jq -r '.tools[:5][]' 2>/dev/null || curl -s http://strikepackage-kali-executor:8002/tools | head -c 200
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Execute nmap version:"
|
||||||
|
cat > /tmp/exec.json << 'EOFEXEC'
|
||||||
|
{"command":"nmap --version","timeout":30}
|
||||||
|
EOFEXEC
|
||||||
|
curl -s -X POST http://strikepackage-kali-executor:8002/execute -H "Content-Type: application/json" -d @/tmp/exec.json | jq -r '.output // .stdout // .' | head -5
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Jobs:"
|
||||||
|
curl -s http://strikepackage-kali-executor:8002/jobs | jq -r 'length' 2>/dev/null || echo "0"
|
||||||
|
echo " jobs recorded"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 5: Security Scans from Kali
|
||||||
|
echo "=== TEST 5: Security Tools in Kali ==="
|
||||||
|
echo "nmap scan of llm-router:"
|
||||||
|
nmap -sT -p 8000 strikepackage-llm-router 2>&1 | grep -E "PORT|open|closed"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "nikto quick test:"
|
||||||
|
nikto -h http://strikepackage-dashboard:8080 -maxtime 10s 2>&1 | tail -5
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "gobuster test:"
|
||||||
|
gobuster dir -u http://strikepackage-hackgpt-api:8001 -w /usr/share/dirb/wordlists/common.txt -t 5 --timeout 5s 2>&1 | grep -E "Status|Found|Finished" | head -10
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 6: Dashboard APIs
|
||||||
|
echo "=== TEST 6: Dashboard APIs ==="
|
||||||
|
echo "Status:"
|
||||||
|
curl -s http://strikepackage-dashboard:8080/api/status | jq . 2>/dev/null || curl -s http://strikepackage-dashboard:8080/api/status
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Processes:"
|
||||||
|
curl -s http://strikepackage-dashboard:8080/api/processes | jq -r 'length' 2>/dev/null
|
||||||
|
echo " processes"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 7: End-to-end Chat via Dashboard
|
||||||
|
echo "=== TEST 7: Dashboard Chat ==="
|
||||||
|
cat > /tmp/dashchat.json << 'EOFDASH'
|
||||||
|
{"message":"What is nmap used for? Answer in one sentence."}
|
||||||
|
EOFDASH
|
||||||
|
CHATRESP=$(curl -s -X POST http://strikepackage-dashboard:8080/api/chat -H "Content-Type: application/json" -d @/tmp/dashchat.json)
|
||||||
|
echo "$CHATRESP" | jq -r '.response // .content // .message // .' 2>/dev/null | head -c 300
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 8: Command Execution via Kali Executor
|
||||||
|
echo "=== TEST 8: Command Execution ==="
|
||||||
|
cat > /tmp/scanexec.json << 'EOFSCAN'
|
||||||
|
{"command":"nmap -sT -p 80,443,8000,8080 strikepackage-dashboard","timeout":60}
|
||||||
|
EOFSCAN
|
||||||
|
echo "Running: nmap scan of dashboard..."
|
||||||
|
EXECRESP=$(curl -s -X POST http://strikepackage-kali-executor:8002/execute -H "Content-Type: application/json" -d @/tmp/scanexec.json)
|
||||||
|
echo "$EXECRESP" | jq -r '.output // .stdout // .' 2>/dev/null | grep -E "PORT|open|closed|filtered" | head -10
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 9: AI-Assisted Scanning
|
||||||
|
echo "=== TEST 9: AI Scan Request ==="
|
||||||
|
cat > /tmp/aiscan.json << 'EOFAI'
|
||||||
|
{"message":"Scan strikepackage-llm-router for web vulnerabilities"}
|
||||||
|
EOFAI
|
||||||
|
AIRESP=$(curl -s -X POST http://strikepackage-dashboard:8080/api/chat -H "Content-Type: application/json" -d @/tmp/aiscan.json)
|
||||||
|
echo "$AIRESP" | jq -r '.response // .content // .' 2>/dev/null | head -c 400
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Tests Complete!"
|
||||||
|
echo "=========================================="
|
||||||
Reference in New Issue
Block a user