mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 22:30:22 -05:00
Add Vite React component bundling, SSE process streaming, preferences persistence, WebSocket terminal proxy, local Ollama integration
- Enable local Ollama service in compose with llm-router dependency - Add SSE /stream/processes endpoint in kali-executor for live process updates - Add WebSocket /ws/execute for real-time terminal command streaming - Implement preferences persistence (provider/model) via dashboard backend - Create Vite build pipeline for React components (VoiceControls, NetworkMap, GuidedWizard) - Update dashboard Dockerfile with Node builder stage for component bundling - Wire dashboard template to mount components and subscribe to SSE/WebSocket streams - Add preferences load/save hooks in UI to persist LLM provider/model selection
This commit is contained in:
156
services/dashboard/components/GuidedWizard.jsx
Normal file
156
services/dashboard/components/GuidedWizard.jsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const WIZARD_TYPES = {
|
||||
first_time_setup: {
|
||||
title: 'Welcome to GooseStrike',
|
||||
steps: [
|
||||
{ id: 'intro', title: 'Introduction', icon: '👋' },
|
||||
{ id: 'phases', title: 'Methodology', icon: '📋' },
|
||||
{ id: 'tools', title: 'Security Tools', icon: '🛠️' },
|
||||
{ id: 'start', title: 'Get Started', icon: '🚀' },
|
||||
],
|
||||
},
|
||||
run_scan: {
|
||||
title: 'Run Security Scan',
|
||||
steps: [
|
||||
{ id: 'target', title: 'Target Selection', icon: '🎯' },
|
||||
{ id: 'scan-type', title: 'Scan Type', icon: '🔍' },
|
||||
{ id: 'options', title: 'Options', icon: '⚙️' },
|
||||
{ id: 'execute', title: 'Execute', icon: '▶️' },
|
||||
],
|
||||
},
|
||||
create_operation: {
|
||||
title: 'Create Security Operation',
|
||||
steps: [
|
||||
{ id: 'details', title: 'Operation Details', icon: '📝' },
|
||||
{ id: 'scope', title: 'Target Scope', icon: '🎯' },
|
||||
{ id: 'methodology', title: 'Methodology', icon: '📋' },
|
||||
{ id: 'review', title: 'Review', icon: '✅' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const GuidedWizard = ({ type = 'first_time_setup', onComplete = () => {}, onCancel = () => {} }) => {
|
||||
const wizard = WIZARD_TYPES[type] || WIZARD_TYPES.first_time_setup;
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [formData, setFormData] = useState({});
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < wizard.steps.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
onComplete(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (key, value) => {
|
||||
setFormData({ ...formData, [key]: value });
|
||||
};
|
||||
|
||||
const progress = ((currentStep + 1) / wizard.steps.length) * 100;
|
||||
|
||||
return (
|
||||
<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;
|
||||
110
services/dashboard/components/NetworkMap.jsx
Normal file
110
services/dashboard/components/NetworkMap.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import cytoscape from 'cytoscape';
|
||||
|
||||
const NetworkMap = ({ hosts = [], onHostSelect = () => {} }) => {
|
||||
const containerRef = useRef(null);
|
||||
const cyRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || hosts.length === 0) return;
|
||||
|
||||
// Build Cytoscape elements from hosts
|
||||
const elements = [];
|
||||
|
||||
hosts.forEach((host) => {
|
||||
elements.push({
|
||||
data: {
|
||||
id: host.ip,
|
||||
label: host.hostname || host.ip,
|
||||
type: host.device_type || 'unknown',
|
||||
os: host.os || 'unknown',
|
||||
ports: host.ports || [],
|
||||
},
|
||||
classes: host.device_type || 'unknown',
|
||||
});
|
||||
|
||||
// Add edges for network relationships (simple example: connect all to a central gateway)
|
||||
if (host.ip !== '192.168.1.1') {
|
||||
elements.push({
|
||||
data: {
|
||||
id: `edge-${host.ip}`,
|
||||
source: '192.168.1.1',
|
||||
target: host.ip,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Cytoscape
|
||||
cyRef.current = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#dc2626',
|
||||
label: 'data(label)',
|
||||
color: '#e5e5e5',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': '10px',
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node.router',
|
||||
style: {
|
||||
'background-color': '#3b82f6',
|
||||
shape: 'diamond',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node.server',
|
||||
style: {
|
||||
'background-color': '#22c55e',
|
||||
shape: 'rectangle',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
width: 2,
|
||||
'line-color': '#3a3a3a',
|
||||
'target-arrow-color': '#3a3a3a',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 500,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle node clicks
|
||||
cyRef.current.on('tap', 'node', (evt) => {
|
||||
const node = evt.target;
|
||||
onHostSelect(node.data());
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [hosts, onHostSelect]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="network-map-container w-full h-full min-h-[500px] rounded border border-sp-grey-mid"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkMap;
|
||||
124
services/dashboard/components/VoiceControls.jsx
Normal file
124
services/dashboard/components/VoiceControls.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => {
|
||||
const [state, setState] = useState('idle'); // idle, listening, processing
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [hotkey, setHotkey] = useState('`'); // backtick
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const audioChunksRef = useRef([]);
|
||||
const hotkeyPressedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
|
||||
hotkeyPressedRef.current = true;
|
||||
startRecording();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.key === hotkey && hotkeyPressedRef.current) {
|
||||
hotkeyPressedRef.current = false;
|
||||
stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [state, hotkey]);
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorderRef.current.ondataavailable = (event) => {
|
||||
audioChunksRef.current.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
||||
await sendToTranscribe(audioBlob);
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.start();
|
||||
setState('listening');
|
||||
setTranscript('Listening...');
|
||||
} catch (error) {
|
||||
console.error('Microphone access denied:', error);
|
||||
setTranscript('Microphone access denied');
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorderRef.current && state === 'listening') {
|
||||
mediaRecorderRef.current.stop();
|
||||
setState('processing');
|
||||
setTranscript('Processing...');
|
||||
}
|
||||
};
|
||||
|
||||
const sendToTranscribe = async (audioBlob) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'recording.wav');
|
||||
|
||||
const response = await fetch(`${apiUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setTranscript(result.text || 'No speech detected');
|
||||
setState('idle');
|
||||
|
||||
if (result.text) {
|
||||
onCommand(result.text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transcription failed:', error);
|
||||
setTranscript('Transcription failed');
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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;
|
||||
40
services/dashboard/components/index.jsx
Normal file
40
services/dashboard/components/index.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import VoiceControls from './VoiceControls';
|
||||
import NetworkMap from './NetworkMap';
|
||||
import GuidedWizard from './GuidedWizard';
|
||||
|
||||
// Export components for external mounting
|
||||
window.GooseStrikeComponents = {
|
||||
VoiceControls,
|
||||
NetworkMap,
|
||||
GuidedWizard,
|
||||
mount: {
|
||||
voiceControls: (containerId, props = {}) => {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<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 };
|
||||
Reference in New Issue
Block a user