3 Commits

Author SHA1 Message Date
5d5a4d4e20 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
2025-12-08 09:07:41 -05:00
26bcb7f947 Merge copilot/expand-strikepackagegpt-implementation into V2.1 2025-12-07 09:25:09 -05:00
ff1b0600a2 Merge pull request #1 from mblanke/C2-integration
Add GitHub Actions workflow to unpack files.zip
2025-12-02 16:24:37 -05:00
22 changed files with 853 additions and 6469 deletions

View File

@@ -1,26 +0,0 @@
version: '3.8'
services:
# Update this to the name of the service you want to work with in your docker-compose.yml file
dashboard:
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
# array). The sample below assumes your primary file is in the root of your project.
#
# build:
# context: .
# dockerfile: .devcontainer/Dockerfile
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
- ..:/workspaces:cached
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
# cap_add:
# - SYS_PTRACE
# security_opt:
# - seccomp:unconfined
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity

View File

@@ -1,12 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

3
.gitignore vendored
View File

@@ -32,6 +32,3 @@ data/
# Temporary files
*.tmp
*.temp
# Nested repo
StrikePackageGPT/

View File

@@ -11,8 +11,6 @@ services:
- HACKGPT_API_URL=http://strikepackage-hackgpt-api:8001
- LLM_ROUTER_URL=http://strikepackage-llm-router:8000
- KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
volumes:
- ./data/dashboard:/app/data
depends_on:
- hackgpt-api
- llm-router
@@ -32,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
@@ -52,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
@@ -69,22 +67,19 @@ services:
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
# Prefer local Ollama container for self-contained setup
- OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://strikepackage-ollama:11434}
# Network Ollama instances (Dell LLM box with larger models)
# 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_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://strikepackage-ollama:11434}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://strikepackage-ollama:11434}
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
# Load balancing: round-robin, random, failover
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-failover}
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin}
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- strikepackage-net
restart: unless-stopped
depends_on:
- ollama
# Kali Linux - Security tools container
kali:
@@ -104,25 +99,26 @@ services:
- NET_RAW
restart: unless-stopped
# Ollama - Local LLM
ollama:
image: ollama/ollama:latest
container_name: strikepackage-ollama
ports:
- "11434:11434"
volumes:
- ollama-models:/root/.ollama
networks:
- strikepackage-net
restart: unless-stopped
# GPU support (optional): uncomment if using NVIDIA GPU
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: all
# capabilities: [gpu]
# Ollama - Local LLM (disabled - using Dell LLM box at 192.168.1.50)
# Uncomment to use local Ollama instead
# ollama:
# image: ollama/ollama:latest
# container_name: strikepackage-ollama
# ports:
# - "11434:11434"
# volumes:
# - ollama-models:/root/.ollama
# networks:
# - strikepackage-net
# restart: unless-stopped
# # Uncomment for GPU support:
# # deploy:
# # resources:
# # reservations:
# # devices:
# # - driver: nvidia
# # count: all
# # capabilities: [gpu]
networks:
strikepackage-net:

View File

@@ -1,14 +1,3 @@
FROM node:20-slim AS builder
WORKDIR /build
# Copy package files and JSX components
COPY package.json vite.config.js ./
COPY components/ ./components/
# Install dependencies and build
RUN npm install && npm run build
FROM python:3.12-slim
WORKDIR /app
@@ -22,9 +11,6 @@ COPY app/ ./app/
COPY templates/ ./templates/
COPY static/ ./static/
# Copy built components from builder stage
COPY --from=builder /build/static/dist/ ./static/dist/
# Expose port
EXPOSE 8080

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +0,0 @@
import React, { useState } from 'react';
const WIZARD_TYPES = {
first_time_setup: {
title: 'Welcome to GooseStrike',
steps: [
{ id: 'intro', title: 'Introduction', icon: '👋' },
{ id: 'phases', title: 'Methodology', icon: '📋' },
{ id: 'tools', title: 'Security Tools', icon: '🛠️' },
{ id: 'start', title: 'Get Started', icon: '🚀' },
],
},
run_scan: {
title: 'Run Security Scan',
steps: [
{ id: 'target', title: 'Target Selection', icon: '🎯' },
{ id: 'scan-type', title: 'Scan Type', icon: '🔍' },
{ id: 'options', title: 'Options', icon: '⚙️' },
{ id: 'execute', title: 'Execute', icon: '▶️' },
],
},
create_operation: {
title: 'Create Security Operation',
steps: [
{ id: 'details', title: 'Operation Details', icon: '📝' },
{ id: 'scope', title: 'Target Scope', icon: '🎯' },
{ id: 'methodology', title: 'Methodology', icon: '📋' },
{ id: 'review', title: 'Review', icon: '✅' },
],
},
};
const GuidedWizard = ({ type = 'first_time_setup', onComplete = () => {}, onCancel = () => {} }) => {
const wizard = WIZARD_TYPES[type] || WIZARD_TYPES.first_time_setup;
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({});
const handleNext = () => {
if (currentStep < wizard.steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onComplete(formData);
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleInputChange = (key, value) => {
setFormData({ ...formData, [key]: value });
};
const progress = ((currentStep + 1) / wizard.steps.length) * 100;
return (
<div className="guided-wizard fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div className="bg-sp-dark rounded-lg border border-sp-grey-mid w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-6 border-b border-sp-grey-mid">
<h2 className="text-2xl font-bold text-sp-white">{wizard.title}</h2>
<div className="mt-4 flex gap-2">
{wizard.steps.map((step, idx) => (
<div
key={step.id}
className={`wizard-step flex-1 p-2 rounded text-center border transition ${
idx === currentStep
? 'border-sp-red bg-sp-red bg-opacity-10'
: idx < currentStep
? 'border-green-500 bg-green-500 bg-opacity-10'
: 'border-sp-grey-mid'
}`}
>
<div className="text-xl">{step.icon}</div>
<div className="text-xs text-sp-white-muted mt-1">{step.title}</div>
</div>
))}
</div>
<div className="mt-3 h-1 bg-sp-grey-mid rounded overflow-hidden">
<div
className="wizard-progress h-full bg-sp-red transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Body */}
<div className="flex-1 p-6 overflow-y-auto">
<div className="text-sp-white">
{/* Render step content based on wizard type and current step */}
{type === 'first_time_setup' && currentStep === 0 && (
<div>
<h3 className="text-xl font-bold mb-4">Welcome to GooseStrike! 🍁</h3>
<p className="text-sp-white-muted mb-4">
GooseStrike is an AI-powered penetration testing platform that follows industry-standard
methodologies to help you identify security vulnerabilities.
</p>
<ul className="list-disc list-inside text-sp-white-muted space-y-2">
<li>AI-assisted security analysis with local or cloud LLMs</li>
<li>600+ integrated Kali Linux security tools</li>
<li>Voice control for hands-free operation</li>
<li>Interactive network visualization</li>
<li>Comprehensive reporting and documentation</li>
</ul>
</div>
)}
{type === 'run_scan' && currentStep === 0 && (
<div>
<h3 className="text-xl font-bold mb-4">Select Target</h3>
<label className="block mb-2 text-sm text-sp-white-muted">Target IP or hostname</label>
<input
type="text"
className="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sp-white"
placeholder="192.168.1.100 or example.com"
value={formData.target || ''}
onChange={(e) => handleInputChange('target', e.target.value)}
/>
</div>
)}
{/* Add more step content as needed */}
</div>
</div>
{/* Footer */}
<div className="p-6 border-t border-sp-grey-mid flex justify-between">
<button
onClick={onCancel}
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
>
Cancel
</button>
<div className="flex gap-2">
{currentStep > 0 && (
<button
onClick={handleBack}
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
>
Back
</button>
)}
<button
onClick={handleNext}
className="px-4 py-2 bg-sp-red hover:bg-sp-red-dark rounded text-sp-white transition"
>
{currentStep === wizard.steps.length - 1 ? 'Complete' : 'Next →'}
</button>
</div>
</div>
</div>
</div>
);
};
export default GuidedWizard;

View File

@@ -1,110 +0,0 @@
import React, { useEffect, useRef } from 'react';
import cytoscape from 'cytoscape';
const NetworkMap = ({ hosts = [], onHostSelect = () => {} }) => {
const containerRef = useRef(null);
const cyRef = useRef(null);
useEffect(() => {
if (!containerRef.current || hosts.length === 0) return;
// Build Cytoscape elements from hosts
const elements = [];
hosts.forEach((host) => {
elements.push({
data: {
id: host.ip,
label: host.hostname || host.ip,
type: host.device_type || 'unknown',
os: host.os || 'unknown',
ports: host.ports || [],
},
classes: host.device_type || 'unknown',
});
// Add edges for network relationships (simple example: connect all to a central gateway)
if (host.ip !== '192.168.1.1') {
elements.push({
data: {
id: `edge-${host.ip}`,
source: '192.168.1.1',
target: host.ip,
},
});
}
});
// Initialize Cytoscape
cyRef.current = cytoscape({
container: containerRef.current,
elements,
style: [
{
selector: 'node',
style: {
'background-color': '#dc2626',
label: 'data(label)',
color: '#e5e5e5',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '10px',
width: 40,
height: 40,
},
},
{
selector: 'node.router',
style: {
'background-color': '#3b82f6',
shape: 'diamond',
},
},
{
selector: 'node.server',
style: {
'background-color': '#22c55e',
shape: 'rectangle',
},
},
{
selector: 'edge',
style: {
width: 2,
'line-color': '#3a3a3a',
'target-arrow-color': '#3a3a3a',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
},
},
],
layout: {
name: 'cose',
animate: true,
animationDuration: 500,
nodeDimensionsIncludeLabels: true,
},
});
// Handle node clicks
cyRef.current.on('tap', 'node', (evt) => {
const node = evt.target;
onHostSelect(node.data());
});
return () => {
if (cyRef.current) {
cyRef.current.destroy();
}
};
}, [hosts, onHostSelect]);
return (
<div
ref={containerRef}
className="network-map-container w-full h-full min-h-[500px] rounded border border-sp-grey-mid"
/>
);
};
export default NetworkMap;

View File

@@ -1,124 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => {
const [state, setState] = useState('idle'); // idle, listening, processing
const [transcript, setTranscript] = useState('');
const [hotkey, setHotkey] = useState('`'); // backtick
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const hotkeyPressedRef = useRef(false);
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
hotkeyPressedRef.current = true;
startRecording();
}
};
const handleKeyUp = (e) => {
if (e.key === hotkey && hotkeyPressedRef.current) {
hotkeyPressedRef.current = false;
stopRecording();
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, [state, hotkey]);
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorderRef.current = new MediaRecorder(stream);
audioChunksRef.current = [];
mediaRecorderRef.current.ondataavailable = (event) => {
audioChunksRef.current.push(event.data);
};
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
await sendToTranscribe(audioBlob);
stream.getTracks().forEach((track) => track.stop());
};
mediaRecorderRef.current.start();
setState('listening');
setTranscript('Listening...');
} catch (error) {
console.error('Microphone access denied:', error);
setTranscript('Microphone access denied');
}
};
const stopRecording = () => {
if (mediaRecorderRef.current && state === 'listening') {
mediaRecorderRef.current.stop();
setState('processing');
setTranscript('Processing...');
}
};
const sendToTranscribe = async (audioBlob) => {
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav');
const response = await fetch(`${apiUrl}/transcribe`, {
method: 'POST',
body: formData,
});
const result = await response.json();
setTranscript(result.text || 'No speech detected');
setState('idle');
if (result.text) {
onCommand(result.text);
}
} catch (error) {
console.error('Transcription failed:', error);
setTranscript('Transcription failed');
setState('idle');
}
};
return (
<div className="voice-controls p-4 bg-sp-grey rounded border border-sp-grey-mid">
<div className="flex items-center gap-3">
<button
className={`voice-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl transition ${
state === 'listening'
? 'bg-sp-red animate-pulse'
: state === 'processing'
? 'bg-yellow-500'
: 'bg-sp-grey-mid hover:bg-sp-red'
}`}
onMouseDown={startRecording}
onMouseUp={stopRecording}
disabled={state === 'processing'}
>
{state === 'listening' ? '🎙️' : state === 'processing' ? '⏳' : '🎤'}
</button>
<div className="flex-1">
<div className="text-sm text-sp-white-muted">
{state === 'idle' && `Press & hold ${hotkey} or click to speak`}
{state === 'listening' && 'Release to stop recording'}
{state === 'processing' && 'Processing audio...'}
</div>
{transcript && (
<div className="text-sm text-sp-white mt-1 font-mono">{transcript}</div>
)}
</div>
</div>
</div>
);
};
export default VoiceControls;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import VoiceControls from './VoiceControls';
import NetworkMap from './NetworkMap';
import GuidedWizard from './GuidedWizard';
// Export components for external mounting
window.GooseStrikeComponents = {
VoiceControls,
NetworkMap,
GuidedWizard,
mount: {
voiceControls: (containerId, props = {}) => {
const container = document.getElementById(containerId);
if (container) {
const root = createRoot(container);
root.render(<VoiceControls {...props} />);
return root;
}
},
networkMap: (containerId, props = {}) => {
const container = document.getElementById(containerId);
if (container) {
const root = createRoot(container);
root.render(<NetworkMap {...props} />);
return root;
}
},
guidedWizard: (containerId, props = {}) => {
const container = document.getElementById(containerId);
if (container) {
const root = createRoot(container);
root.render(<GuidedWizard {...props} />);
return root;
}
},
},
};
export { VoiceControls, NetworkMap, GuidedWizard };

View File

@@ -1,19 +0,0 @@
{
"name": "goosestrike-dashboard",
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"cytoscape": "^3.28.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.10"
}
}

View File

@@ -3,4 +3,3 @@ uvicorn[standard]==0.32.1
httpx==0.28.1
pydantic==2.10.2
jinja2==3.1.4
websockets==12.0

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'static/dist',
emptyOutDir: true,
rollupOptions: {
input: {
components: path.resolve(__dirname, 'components/index.jsx'),
},
output: {
entryFileNames: 'components.js',
chunkFileNames: 'components-[name].js',
assetFileNames: 'components-[name].[ext]',
},
},
},
});

View File

@@ -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_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)
tasks: Dict[str, Any] = {}

View File

@@ -4,7 +4,6 @@ Executes commands in the Kali container via Docker SDK.
"""
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
import docker
@@ -14,8 +13,6 @@ import os
import uuid
import json
import re
import httpx
import xml.etree.ElementTree as ET
from datetime import datetime
from contextlib import asynccontextmanager
@@ -104,307 +101,6 @@ def validate_command(command: str) -> tuple[bool, str]:
return True, "OK"
# Dashboard URL for sending discovered hosts
DASHBOARD_URL = os.getenv("DASHBOARD_URL", "http://dashboard:8080")
def is_nmap_command(command: str) -> bool:
"""Check if command is an nmap scan that might discover hosts."""
parts = command.strip().split()
if not parts:
return False
base_cmd = parts[0].split("/")[-1]
return base_cmd == "nmap" or base_cmd == "masscan"
def detect_os_type(os_string: str) -> str:
"""Detect OS type from nmap OS string."""
if not os_string:
return ""
os_lower = os_string.lower()
if "windows" in os_lower:
return "Windows"
elif any(x in os_lower for x in ["linux", "ubuntu", "debian", "centos", "red hat"]):
return "Linux"
elif any(x in os_lower for x in ["mac os", "darwin", "apple", "ios"]):
return "macOS"
elif "cisco" in os_lower:
return "Cisco Router"
elif "juniper" in os_lower:
return "Juniper Router"
elif any(x in os_lower for x in ["fortinet", "fortigate"]):
return "Fortinet"
elif any(x in os_lower for x in ["vmware", "esxi"]):
return "VMware Server"
elif "freebsd" in os_lower:
return "FreeBSD"
elif "android" in os_lower:
return "Android"
elif any(x in os_lower for x in ["printer", "hp"]):
return "Printer"
elif "switch" in os_lower:
return "Network Switch"
elif "router" in os_lower:
return "Router"
return ""
def infer_os_from_ports(ports: List[Dict]) -> str:
"""Infer OS type from open ports.
Uses a scoring system to handle hosts running multiple services
(e.g., Linux with Samba looks like Windows on port 445).
"""
port_nums = {p["port"] for p in ports}
services = {p.get("service", "").lower() for p in ports}
products = [p.get("product", "").lower() for p in ports]
# Score-based detection to handle mixed indicators
linux_score = 0
windows_score = 0
# Strong Linux indicators
if 22 in port_nums: # SSH is strongly Linux/Unix
linux_score += 3
if any("openssh" in p or "linux" in p for p in products):
linux_score += 5
if any("apache" in p or "nginx" in p for p in products):
linux_score += 2
# Strong Windows indicators
if 135 in port_nums: # MSRPC is Windows-only
windows_score += 5
if 3389 in port_nums: # RDP is Windows
windows_score += 3
if 5985 in port_nums or 5986 in port_nums: # WinRM is Windows-only
windows_score += 5
if any("microsoft" in p or "windows" in p for p in products):
windows_score += 5
# Weak indicators (could be either)
if 445 in port_nums: # SMB - could be Samba on Linux or Windows
windows_score += 1 # Slight Windows bias but not definitive
if 139 in port_nums: # NetBIOS - same as above
windows_score += 1
# Decide based on score
if linux_score > windows_score:
return "Linux"
if windows_score > linux_score:
return "Windows"
# Network device indicators
if 161 in port_nums or 162 in port_nums:
return "Network Device"
# Printer
if 9100 in port_nums or 631 in port_nums:
return "Printer"
return ""
def parse_nmap_output(stdout: str) -> List[Dict[str, Any]]:
"""Parse nmap output (XML or text) and extract discovered hosts."""
hosts = []
# Try XML parsing first (if -oX - was used or combined with other options)
if '<?xml' in stdout or '<nmaprun' in stdout:
try:
xml_start = stdout.find('<?xml')
if xml_start == -1:
xml_start = stdout.find('<nmaprun')
if xml_start != -1:
xml_output = stdout[xml_start:]
hosts = parse_nmap_xml(xml_output)
if hosts:
return hosts
except Exception as e:
print(f"XML parsing failed: {e}")
# Fallback to text parsing
hosts = parse_nmap_text(stdout)
return hosts
def parse_nmap_xml(xml_output: str) -> List[Dict[str, Any]]:
"""Parse nmap XML output to extract hosts."""
hosts = []
try:
root = ET.fromstring(xml_output)
for host_elem in root.findall('.//host'):
status = host_elem.find("status")
if status is None or status.get("state") != "up":
continue
host = {
"ip": "",
"hostname": "",
"mac": "",
"vendor": "",
"os_type": "",
"os_details": "",
"ports": []
}
# Get IP address
addr = host_elem.find("address[@addrtype='ipv4']")
if addr is not None:
host["ip"] = addr.get("addr", "")
# Get MAC address
mac = host_elem.find("address[@addrtype='mac']")
if mac is not None:
host["mac"] = mac.get("addr", "")
host["vendor"] = mac.get("vendor", "")
# Get hostname
hostname = host_elem.find(".//hostname")
if hostname is not None:
host["hostname"] = hostname.get("name", "")
# Get OS info
os_elem = host_elem.find(".//osmatch")
if os_elem is not None:
os_name = os_elem.get("name", "")
host["os_details"] = os_name
host["os_type"] = detect_os_type(os_name)
# Get ports
for port_elem in host_elem.findall(".//port"):
state_elem = port_elem.find("state")
port_info = {
"port": int(port_elem.get("portid", 0)),
"protocol": port_elem.get("protocol", "tcp"),
"state": state_elem.get("state", "") if state_elem is not None else "",
"service": ""
}
service = port_elem.find("service")
if service is not None:
port_info["service"] = service.get("name", "")
port_info["product"] = service.get("product", "")
port_info["version"] = service.get("version", "")
if port_info["state"] == "open":
host["ports"].append(port_info)
# Infer OS from ports if still unknown
if not host["os_type"] and host["ports"]:
host["os_type"] = infer_os_from_ports(host["ports"])
# Only include hosts with at least one OPEN port
# This prevents false positives from proxy ARP responses
# where routers respond for all IPs even if device is offline
if host["ip"] and host["ports"]:
hosts.append(host)
except ET.ParseError as e:
print(f"XML parse error: {e}")
return hosts
def parse_nmap_text(output: str) -> List[Dict[str, Any]]:
"""Parse nmap text output as fallback.
Only returns hosts that have at least one OPEN port.
Filters out false positives from router proxy ARP (where all IPs appear "up").
"""
hosts = []
current_host = None
def save_host_if_has_open_ports(host):
"""Only save host if it has at least one open port."""
if host and host.get("ip") and host.get("ports"):
# Infer OS before saving
if not host["os_type"]:
host["os_type"] = infer_os_from_ports(host["ports"])
hosts.append(host)
for line in output.split('\n'):
# Match host line: "Nmap scan report for hostname (IP)" or "Nmap scan report for IP"
host_match = re.search(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)', line)
if host_match:
# Save previous host only if it has open ports
save_host_if_has_open_ports(current_host)
current_host = {
"ip": host_match.group(2),
"hostname": host_match.group(1) or "",
"os_type": "",
"os_details": "",
"ports": [],
"mac": "",
"vendor": ""
}
continue
if current_host:
# Match MAC: "MAC Address: XX:XX:XX:XX:XX:XX (Vendor Name)"
mac_match = re.search(r'MAC Address: ([0-9A-Fa-f:]+)(?: \(([^)]+)\))?', line)
if mac_match:
current_host["mac"] = mac_match.group(1)
current_host["vendor"] = mac_match.group(2) or ""
# Match port: "80/tcp open http Apache httpd"
port_match = re.search(r'(\d+)/(tcp|udp)\s+(\w+)\s+(\S+)(?:\s+(.*))?', line)
if port_match and port_match.group(3) == "open":
port_info = {
"port": int(port_match.group(1)),
"protocol": port_match.group(2),
"state": "open",
"service": port_match.group(4),
"product": port_match.group(5) or ""
}
current_host["ports"].append(port_info)
# Match OS: "OS details: Linux 4.15 - 5.6" or "Running: Linux"
os_match = re.search(r'(?:OS details?|Running):\s*(.+)', line)
if os_match:
current_host["os_details"] = os_match.group(1)
current_host["os_type"] = detect_os_type(os_match.group(1))
# Match "Service Info: OS: Linux" style
service_os_match = re.search(r'Service Info:.*OS:\s*([^;,]+)', line)
if service_os_match and not current_host["os_type"]:
current_host["os_type"] = detect_os_type(service_os_match.group(1))
# Match "Aggressive OS guesses: Linux 5.4 (98%)" - take first high confidence
aggressive_match = re.search(r'Aggressive OS guesses:\s*([^(]+)\s*\((\d+)%\)', line)
if aggressive_match and not current_host["os_details"]:
confidence = int(aggressive_match.group(2))
if confidence >= 85:
current_host["os_details"] = aggressive_match.group(1).strip()
current_host["os_type"] = detect_os_type(aggressive_match.group(1))
# Don't forget the last host - only if it has open ports
save_host_if_has_open_ports(current_host)
return hosts
async def send_hosts_to_dashboard(hosts: List[Dict[str, Any]]):
"""Send discovered hosts to the dashboard for network map update."""
if not hosts:
return
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{DASHBOARD_URL}/api/network/hosts/discover",
json={"hosts": hosts, "source": "terminal"}
)
if response.status_code == 200:
result = response.json()
print(f"Sent {len(hosts)} hosts to dashboard: added={result.get('added')}, updated={result.get('updated')}")
else:
print(f"Failed to send hosts to dashboard: {response.status_code}")
except Exception as e:
print(f"Error sending hosts to dashboard: {e}")
# Docker client
docker_client = None
kali_container = None
@@ -557,23 +253,6 @@ def _run_command_sync(container, command, working_dir):
workdir=working_dir
)
@app.get("/stream/processes")
async def stream_running_processes():
"""Server-Sent Events stream of running security processes.
Emits JSON events with current process list every 5 seconds.
"""
async def event_generator():
while True:
try:
data = await get_running_processes()
yield f"data: {json.dumps(data)}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
await asyncio.sleep(5)
return StreamingResponse(event_generator(), media_type="text/event-stream")
@app.post("/execute", response_model=CommandResult)
async def execute_command(request: CommandRequest):
"""Execute a command in the Kali container."""
@@ -613,15 +292,6 @@ async def execute_command(request: CommandRequest):
stdout = output[0].decode('utf-8', errors='replace') if output[0] else ""
stderr = output[1].decode('utf-8', errors='replace') if output[1] else ""
# Parse nmap output and send hosts to dashboard for network map
if is_nmap_command(request.command) and stdout:
try:
hosts = parse_nmap_output(stdout)
if hosts:
asyncio.create_task(send_hosts_to_dashboard(hosts))
except Exception as e:
print(f"Error parsing nmap output: {e}")
return CommandResult(
command_id=command_id,
command=request.command,
@@ -639,52 +309,6 @@ async def execute_command(request: CommandRequest):
except Exception as e:
raise HTTPException(status_code=500, detail=f"Execution error: {str(e)}")
@app.websocket("/ws/execute/{command_id}")
async def websocket_execute(websocket: WebSocket, command_id: str):
"""WebSocket endpoint for streaming command output in real-time."""
await websocket.accept()
if command_id not in running_commands:
await websocket.send_json({"error": "Command not found"})
await websocket.close()
return
cmd_info = running_commands[command_id]
try:
# Stream output as it becomes available
last_stdout_len = 0
last_stderr_len = 0
while cmd_info["status"] == "running":
current_stdout = cmd_info.get("stdout", "")
current_stderr = cmd_info.get("stderr", "")
# Send new stdout
if len(current_stdout) > last_stdout_len:
new_stdout = current_stdout[last_stdout_len:]
await websocket.send_json({"type": "stdout", "data": new_stdout})
last_stdout_len = len(current_stdout)
# Send new stderr
if len(current_stderr) > last_stderr_len:
new_stderr = current_stderr[last_stderr_len:]
await websocket.send_json({"type": "stderr", "data": new_stderr})
last_stderr_len = len(current_stderr)
await asyncio.sleep(0.5)
# Send final status
await websocket.send_json({
"type": "complete",
"status": cmd_info["status"],
"exit_code": cmd_info.get("exit_code"),
"duration": cmd_info.get("duration_seconds"),
})
except WebSocketDisconnect:
pass
finally:
await websocket.close()
@app.post("/execute/async")
async def execute_command_async(request: CommandRequest):
@@ -796,92 +420,18 @@ async def websocket_execute(websocket: WebSocket):
workdir=working_dir
)
# Collect output for nmap parsing
full_stdout = []
is_nmap = is_nmap_command(command)
# Stream output with keepalive for long-running commands
last_output_time = asyncio.get_event_loop().time()
output_queue = asyncio.Queue()
stream_complete = asyncio.Event()
# Synchronous function to read from Docker stream (runs in thread)
def read_docker_output_sync(queue: asyncio.Queue, loop, complete_event):
try:
for stdout, stderr in exec_result.output:
if stdout:
asyncio.run_coroutine_threadsafe(
queue.put(("stdout", stdout.decode('utf-8', errors='replace'))),
loop
)
if stderr:
asyncio.run_coroutine_threadsafe(
queue.put(("stderr", stderr.decode('utf-8', errors='replace'))),
loop
)
except Exception as e:
asyncio.run_coroutine_threadsafe(
queue.put(("error", str(e))),
loop
)
finally:
loop.call_soon_threadsafe(complete_event.set)
# Start reading in background thread
loop = asyncio.get_event_loop()
read_future = loop.run_in_executor(
executor,
read_docker_output_sync,
output_queue,
loop,
stream_complete
)
# Send output and keepalives
keepalive_interval = 25 # seconds
while not stream_complete.is_set() or not output_queue.empty():
try:
# Wait for output with timeout for keepalive
try:
msg_type, msg_data = await asyncio.wait_for(
output_queue.get(),
timeout=keepalive_interval
)
last_output_time = asyncio.get_event_loop().time()
if msg_type == "stdout":
if is_nmap:
full_stdout.append(msg_data)
await websocket.send_json({"type": "stdout", "data": msg_data})
elif msg_type == "stderr":
await websocket.send_json({"type": "stderr", "data": msg_data})
elif msg_type == "error":
await websocket.send_json({"type": "error", "message": msg_data})
except asyncio.TimeoutError:
# No output for a while, send keepalive
elapsed = asyncio.get_event_loop().time() - last_output_time
await websocket.send_json({
"type": "keepalive",
"elapsed": int(elapsed),
"message": f"Scan in progress ({int(elapsed)}s)..."
})
except Exception as e:
print(f"Error in output loop: {e}")
break
# Wait for read thread to complete
await read_future
# Parse nmap output and send hosts to dashboard
if is_nmap and full_stdout:
try:
combined_output = "".join(full_stdout)
hosts = parse_nmap_output(combined_output)
if hosts:
asyncio.create_task(send_hosts_to_dashboard(hosts))
except Exception as e:
print(f"Error parsing nmap output: {e}")
# Stream output
for stdout, stderr in exec_result.output:
if stdout:
await websocket.send_json({
"type": "stdout",
"data": stdout.decode('utf-8', errors='replace')
})
if stderr:
await websocket.send_json({
"type": "stderr",
"data": stderr.decode('utf-8', errors='replace')
})
await websocket.send_json({
"type": "complete",

View File

@@ -3,4 +3,3 @@ uvicorn[standard]==0.32.1
docker==7.1.0
pydantic==2.10.2
websockets==14.1
httpx==0.28.1

View File

@@ -3,45 +3,56 @@ FROM kalilinux/kali-rolling
# Avoid prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Configure apt to use reliable mirrors and retry on failure
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries && \
echo 'Acquire::http::Timeout "30";' >> /etc/apt/apt.conf.d/80-retries && \
echo 'deb http://kali.download/kali kali-rolling main non-free non-free-firmware contrib' > /etc/apt/sources.list
# Configure apt to use direct Kali mirrors (avoid CDN mirrors that get blocked by content filters)
# 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
# Install kali-linux-everything metapackage (600+ tools, ~15GB)
# This includes: nmap, metasploit, burpsuite, wireshark, aircrack-ng,
# hashcat, john, hydra, sqlmap, nikto, wpscan, responder, crackmapexec,
# enum4linux, gobuster, dirb, wfuzz, masscan, and hundreds more
RUN apt-get update && \
apt-get install -y kali-linux-everything && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Update and install essential security tools
RUN apt-get update && apt-get install -y --no-install-recommends \
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 \
&& rm -rf /var/lib/apt/lists/*
# Install additional Python tools and utilities for command logging
# Install setuptools first to fix compatibility issues with Python 3.13
RUN pip3 install --break-system-packages setuptools wheel && \
pip3 install --break-system-packages \
# Install additional Python tools for command logging and scripting
RUN pip3 install --break-system-packages \
requests \
beautifulsoup4 \
shodan \
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
WORKDIR /workspace
# Copy scripts and fix line endings (in case of Windows CRLF)
# Copy scripts and convert Windows line endings to Unix
COPY entrypoint.sh /entrypoint.sh
COPY command_logger.sh /usr/local/bin/command_logger.sh
COPY capture_wrapper.sh /usr/local/bin/capture
RUN sed -i 's/\r$//' /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture && \
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
RUN mkdir -p /workspace/.command_history

View File

@@ -1,8 +1,10 @@
#!/bin/bash
# Output Capture Wrapper for Security Tools
# 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}"
DASHBOARD_URL="${DASHBOARD_URL:-http://strikepackage-dashboard:8080}"
mkdir -p "$COMMAND_LOG_DIR"
# Get command from arguments
@@ -61,12 +63,35 @@ cat > "$output_file" << EOF
}
EOF
# Output results to terminal first
echo "$stdout_content"
[ -n "$stderr_content" ] && echo "$stderr_content" >&2
# Clean up temp files
rm -f "$stdout_file" "$stderr_file"
# Output results to terminal
cat "$stdout_file" 2>/dev/null || true
cat "$stderr_file" >&2 2>/dev/null || true
# If this was an nmap command, send results to dashboard network map
if [[ "$cmd_string" == nmap* ]] && [ $exit_code -eq 0 ]; then
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 "[StrikePackageGPT] Command captured: $cmd_id" >&2

View File

@@ -3,12 +3,48 @@
# Enable command logging by default for all bash sessions
echo 'source /usr/local/bin/command_logger.sh' >> /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
cat >> /root/.bashrc << 'ALIASES'
# Convenience alias to run commands with automatic 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
recent_commands() {
echo "Recent commands logged:"
@@ -18,36 +54,42 @@ recent_commands() {
done
}
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
echo "=================================================="
echo " StrikePackageGPT - Kali Container"
echo " Security Tools Ready + Command Capture Enabled"
echo " Security Tools Ready + Network Map Integration"
echo "=================================================="
echo ""
echo "Available tools:"
echo " - nmap, masscan (port scanning)"
echo " - amass, theharvester (reconnaissance)"
echo " - nikto, gobuster (web testing)"
echo " - sqlmap (SQL injection)"
echo " - hydra (brute force)"
echo " - metasploit (exploitation)"
echo " - searchsploit (exploit database)"
echo " - aircrack-ng, wifite (wireless)"
echo " - john, hashcat (password cracking)"
echo " - and 600+ more Kali tools"
echo ""
echo "🔄 BIDIRECTIONAL CAPTURE ENABLED 🔄"
echo "🗺️ NETWORK MAP INTEGRATION ENABLED 🗺️"
echo ""
echo "Commands you run here will be captured and visible in:"
echo " • Dashboard history"
echo " • API scan results"
echo " • Network visualization"
echo "nmap scans automatically appear in the Dashboard Network Map!"
echo ""
echo "Usage:"
echo " • Run commands normally: nmap -sV 192.168.1.1"
echo " • Use 'capture' prefix for explicit capture: capture nmap -sV 192.168.1.1"
echo " • View recent: recent"
echo "Commands:"
echo " • nmap -sV 192.168.1.1 - Scan and auto-add to map"
echo " • hosts - Show network map hosts"
echo " • clear_hosts - Clear network map"
echo " • recent - Show recent commands"
echo ""
echo "Container is ready for security testing."
echo ""

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