diff --git a/services/dashboard/ExplainButton.jsx b/services/dashboard/ExplainButton.jsx new file mode 100644 index 0000000..efdcccc --- /dev/null +++ b/services/dashboard/ExplainButton.jsx @@ -0,0 +1,345 @@ +/** + * ExplainButton Component + * Reusable inline "Explain" button for configs, logs, and errors + * Shows modal/popover with LLM-powered explanation + */ + +import React, { useState } from 'react'; + +const ExplainButton = ({ + type = 'config', // config, log, error, scan_result + content, + context = {}, + size = 'small', + style = {} +}) => { + const [isLoading, setIsLoading] = useState(false); + const [showModal, setShowModal] = useState(false); + const [explanation, setExplanation] = useState(null); + const [error, setError] = useState(null); + + const handleExplain = async () => { + setIsLoading(true); + setError(null); + setShowModal(true); + + try { + const response = await fetch('/api/explain', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type, + content, + context + }) + }); + + if (!response.ok) { + throw new Error('Failed to get explanation'); + } + + const data = await response.json(); + setExplanation(data); + } catch (err) { + console.error('Error getting explanation:', err); + setError('Failed to load explanation. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const closeModal = () => { + setShowModal(false); + setExplanation(null); + setError(null); + }; + + const buttonSizes = { + small: { padding: '4px 8px', fontSize: '12px' }, + medium: { padding: '6px 12px', fontSize: '14px' }, + large: { padding: '8px 16px', fontSize: '16px' } + }; + + const buttonStyle = { + ...buttonSizes[size], + backgroundColor: '#3498DB', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + transition: 'background-color 0.2s', + ...style + }; + + const renderExplanation = () => { + if (error) { + return ( +
+ {error} +
+ ); + } + + if (isLoading) { + return ( +
+
+
Generating explanation...
+
+ ); + } + + if (!explanation) { + return null; + } + + // Render based on explanation type + switch (type) { + case 'config': + return ( +
+

+ {explanation.config_key || 'Configuration'} +

+ +
+ Current Value: + + {explanation.current_value} + +
+ +
+ What it does: +

{explanation.description}

+
+ + {explanation.example && ( +
+ Example: +

{explanation.example}

+
+ )} + + {explanation.value_analysis && ( +
+ Analysis: {explanation.value_analysis} +
+ )} + + {explanation.recommendations && explanation.recommendations.length > 0 && ( +
+ Recommendations: + +
+ )} + +
+ {explanation.requires_restart && ( +
⚠️ Changing this setting requires a restart
+ )} + {!explanation.safe_to_change && ( +
⚠️ Use caution when changing this setting
+ )} +
+
+ ); + + case 'error': + return ( +
+

+ Error Explanation +

+ +
+ Original Error: +
+ {explanation.original_error} +
+
+ +
+ What went wrong: +

{explanation.plain_english}

+
+ +
+ Likely causes: + +
+ +
+ 💡 How to fix it: +
    + {explanation.suggested_fixes?.map((fix, i) => ( +
  1. {fix}
  2. + ))} +
+
+ +
+ Severity: + {(explanation.severity || 'unknown').toUpperCase()} + +
+
+ ); + + case 'log': + return ( +
+

+ Log Entry Explanation +

+ +
+ {explanation.log_entry} +
+ +
+ Level: + + {explanation.log_level} + +
+ + {explanation.timestamp && ( +
+ Time: {explanation.timestamp} +
+ )} + +
+ What this means: +

{explanation.explanation}

+
+ + {explanation.action_needed && explanation.next_steps && explanation.next_steps.length > 0 && ( +
+ ⚠️ Action needed: + +
+ )} +
+ ); + + default: + return ( +
+
{explanation.explanation || 'No explanation available.'}
+
+ ); + } + }; + + return ( + <> + + + {showModal && ( +
+
e.stopPropagation()} + > +
+

Explanation

+ +
+ + {renderExplanation()} +
+
+ )} + + ); +}; + +export default ExplainButton; diff --git a/services/dashboard/GuidedWizard.jsx b/services/dashboard/GuidedWizard.jsx new file mode 100644 index 0000000..9f72fd9 --- /dev/null +++ b/services/dashboard/GuidedWizard.jsx @@ -0,0 +1,487 @@ +/** + * GuidedWizard Component + * Multi-step wizard for onboarding flows + * Types: create_operation, onboard_agent, run_scan, first_time_setup + */ + +import React, { useState, useEffect } from 'react'; + +const GuidedWizard = ({ + wizardType = 'first_time_setup', + onComplete, + onCancel, + initialData = {} +}) => { + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState(initialData); + const [stepHelp, setStepHelp] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const wizardConfigs = { + create_operation: { + title: 'Create New Operation', + steps: [ + { + number: 1, + title: 'Operation Name and Type', + fields: [ + { name: 'operation_name', label: 'Operation Name', type: 'text', required: true, placeholder: 'Q4 Security Assessment' }, + { name: 'operation_type', label: 'Operation Type', type: 'select', required: true, options: [ + { value: 'external', label: 'External Penetration Test' }, + { value: 'internal', label: 'Internal Network Assessment' }, + { value: 'webapp', label: 'Web Application Test' }, + { value: 'wireless', label: 'Wireless Security Assessment' } + ]} + ] + }, + { + number: 2, + title: 'Define Target Scope', + fields: [ + { name: 'target_range', label: 'Target Network Range', type: 'text', required: true, placeholder: '192.168.1.0/24' }, + { name: 'excluded_hosts', label: 'Excluded Hosts (comma-separated)', type: 'text', placeholder: '192.168.1.1, 192.168.1.254' }, + { name: 'domains', label: 'Target Domains', type: 'textarea', placeholder: 'example.com\napp.example.com' } + ] + }, + { + number: 3, + title: 'Configure Assessment Tools', + fields: [ + { name: 'scan_intensity', label: 'Scan Intensity', type: 'select', required: true, options: [ + { value: '1', label: 'Stealth (Slowest, least detectable)' }, + { value: '3', label: 'Balanced (Recommended)' }, + { value: '5', label: 'Aggressive (Fastest, easily detected)' } + ]}, + { name: 'tools', label: 'Tools to Use', type: 'multiselect', options: [ + { value: 'nmap', label: 'Nmap (Network Scanning)' }, + { value: 'nikto', label: 'Nikto (Web Server Scanning)' }, + { value: 'gobuster', label: 'Gobuster (Directory Enumeration)' }, + { value: 'sqlmap', label: 'SQLMap (SQL Injection Testing)' } + ]} + ] + } + ] + }, + run_scan: { + title: 'Run Security Scan', + steps: [ + { + number: 1, + title: 'Select Scan Tool', + fields: [ + { name: 'tool', label: 'Security Tool', type: 'select', required: true, options: [ + { value: 'nmap', label: 'Nmap - Network Scanner' }, + { value: 'nikto', label: 'Nikto - Web Server Scanner' }, + { value: 'gobuster', label: 'Gobuster - Directory/File Discovery' }, + { value: 'sqlmap', label: 'SQLMap - SQL Injection' }, + { value: 'whatweb', label: 'WhatWeb - Technology Detection' } + ]} + ] + }, + { + number: 2, + title: 'Specify Target', + fields: [ + { name: 'target', label: 'Target', type: 'text', required: true, placeholder: '192.168.1.0/24 or example.com' }, + { name: 'ports', label: 'Ports (optional)', type: 'text', placeholder: '80,443,8080 or 1-1000' } + ] + }, + { + number: 3, + title: 'Scan Options', + fields: [ + { name: 'scan_type', label: 'Scan Type', type: 'select', required: true, options: [ + { value: 'quick', label: 'Quick Scan (Fast, common ports)' }, + { value: 'full', label: 'Full Scan (Comprehensive, slower)' }, + { value: 'stealth', label: 'Stealth Scan (Slow, harder to detect)' }, + { value: 'vuln', label: 'Vulnerability Scan (Checks for known vulns)' } + ]}, + { name: 'timeout', label: 'Timeout (seconds)', type: 'number', placeholder: '300' } + ] + } + ] + }, + first_time_setup: { + title: 'Welcome to StrikePackageGPT', + steps: [ + { + number: 1, + title: 'Welcome', + fields: [ + { name: 'user_name', label: 'Your Name', type: 'text', placeholder: 'John Doe' }, + { name: 'skill_level', label: 'Security Testing Experience', type: 'select', required: true, options: [ + { value: 'beginner', label: 'Beginner - Learning the basics' }, + { value: 'intermediate', label: 'Intermediate - Some experience' }, + { value: 'advanced', label: 'Advanced - Professional pentester' } + ]} + ] + }, + { + number: 2, + title: 'Configure LLM Provider', + fields: [ + { name: 'llm_provider', label: 'LLM Provider', type: 'select', required: true, options: [ + { value: 'ollama', label: 'Ollama (Local, Free)' }, + { value: 'openai', label: 'OpenAI (Cloud, Requires API Key)' }, + { value: 'anthropic', label: 'Anthropic Claude (Cloud, Requires API Key)' } + ]}, + { name: 'api_key', label: 'API Key (if using cloud provider)', type: 'password', placeholder: 'sk-...' } + ] + }, + { + number: 3, + title: 'Review and Finish', + fields: [] + } + ] + } + }; + + const config = wizardConfigs[wizardType] || wizardConfigs.first_time_setup; + const totalSteps = config.steps.length; + const currentStepConfig = config.steps[currentStep - 1]; + + useEffect(() => { + fetchStepHelp(); + }, [currentStep]); + + const fetchStepHelp = async () => { + try { + const response = await fetch(`/api/wizard/help?type=${wizardType}&step=${currentStep}`); + if (response.ok) { + const data = await response.json(); + setStepHelp(data); + } + } catch (err) { + console.error('Failed to fetch step help:', err); + } + }; + + const handleFieldChange = (fieldName, value) => { + setFormData(prev => ({ ...prev, [fieldName]: value })); + }; + + const validateCurrentStep = () => { + const requiredFields = currentStepConfig.fields.filter(f => f.required); + for (const field of requiredFields) { + if (!formData[field.name]) { + setError(`${field.label} is required`); + return false; + } + } + setError(null); + return true; + }; + + const handleNext = () => { + if (!validateCurrentStep()) return; + + if (currentStep < totalSteps) { + setCurrentStep(prev => prev + 1); + } else { + handleComplete(); + } + }; + + const handleBack = () => { + if (currentStep > 1) { + setCurrentStep(prev => prev - 1); + setError(null); + } + }; + + const handleComplete = async () => { + if (!validateCurrentStep()) return; + + setLoading(true); + try { + if (onComplete) { + await onComplete(formData); + } + } catch (err) { + setError('Failed to complete wizard: ' + err.message); + } finally { + setLoading(false); + } + }; + + const renderField = (field) => { + const commonStyle = { + width: '100%', + padding: '10px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px' + }; + + switch (field.type) { + case 'text': + case 'password': + case 'number': + return ( + handleFieldChange(field.name, e.target.value)} + placeholder={field.placeholder} + style={commonStyle} + /> + ); + + case 'textarea': + return ( +