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:
2025-12-28 21:29:59 -05:00
parent b971482bbd
commit af31caeacf
10 changed files with 1144 additions and 25 deletions

View 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;

View 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;

View 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;

View 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 };