Add backend modules and frontend components for StrikePackageGPT expansion

Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-03 12:50:53 +00:00
parent 7b75477450
commit f49b63e7af
18 changed files with 4506 additions and 0 deletions

View File

@@ -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 (
<div style={{ color: '#E74C3C', padding: '10px' }}>
{error}
</div>
);
}
if (isLoading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div style={{ fontSize: '24px', marginBottom: '10px' }}></div>
<div>Generating explanation...</div>
</div>
);
}
if (!explanation) {
return null;
}
// Render based on explanation type
switch (type) {
case 'config':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
{explanation.config_key || 'Configuration'}
</h3>
<div style={{ marginBottom: '15px' }}>
<strong>Current Value:</strong>
<code style={{
backgroundColor: '#f5f5f5',
padding: '2px 6px',
borderRadius: '3px',
marginLeft: '5px'
}}>
{explanation.current_value}
</code>
</div>
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>What it does:</strong>
<p style={{ margin: '5px 0' }}>{explanation.description}</p>
</div>
{explanation.example && (
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>Example:</strong>
<p style={{ margin: '5px 0', fontStyle: 'italic' }}>{explanation.example}</p>
</div>
)}
{explanation.value_analysis && (
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F4F8', borderRadius: '4px' }}>
<strong>Analysis:</strong> {explanation.value_analysis}
</div>
)}
{explanation.recommendations && explanation.recommendations.length > 0 && (
<div style={{ marginBottom: '15px' }}>
<strong>Recommendations:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.recommendations.map((rec, i) => (
<li key={i} style={{ margin: '5px 0' }}>{rec}</li>
))}
</ul>
</div>
)}
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px', paddingTop: '15px', borderTop: '1px solid #ddd' }}>
{explanation.requires_restart && (
<div> Changing this setting requires a restart</div>
)}
{!explanation.safe_to_change && (
<div> Use caution when changing this setting</div>
)}
</div>
</div>
);
case 'error':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px', color: '#E74C3C' }}>
Error Explanation
</h3>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#fef5e7', borderRadius: '4px', fontSize: '14px' }}>
<strong>Original Error:</strong>
<div style={{ marginTop: '5px', fontFamily: 'monospace', fontSize: '12px' }}>
{explanation.original_error}
</div>
</div>
<div style={{ marginBottom: '15px' }}>
<strong>What went wrong:</strong>
<p style={{ margin: '5px 0', lineHeight: '1.6' }}>{explanation.plain_english}</p>
</div>
<div style={{ marginBottom: '15px' }}>
<strong>Likely causes:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.likely_causes?.map((cause, i) => (
<li key={i} style={{ margin: '5px 0' }}>{cause}</li>
))}
</ul>
</div>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F8F5', borderRadius: '4px' }}>
<strong>💡 How to fix it:</strong>
<ol style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.suggested_fixes?.map((fix, i) => (
<li key={i} style={{ margin: '5px 0' }}>{fix}</li>
))}
</ol>
</div>
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px' }}>
Severity: <span style={{
color: explanation.severity === 'critical' ? '#E74C3C' :
explanation.severity === 'high' ? '#E67E22' :
explanation.severity === 'medium' ? '#F39C12' : '#95A5A6',
fontWeight: 'bold'
}}>
{(explanation.severity || 'unknown').toUpperCase()}
</span>
</div>
</div>
);
case 'log':
return (
<div style={{ padding: '15px' }}>
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
Log Entry Explanation
</h3>
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px', fontSize: '13px', fontFamily: 'monospace' }}>
{explanation.log_entry}
</div>
<div style={{ marginBottom: '15px' }}>
<strong>Level:</strong>
<span style={{
marginLeft: '5px',
padding: '2px 8px',
borderRadius: '3px',
backgroundColor: explanation.log_level === 'ERROR' ? '#E74C3C' :
explanation.log_level === 'WARNING' ? '#F39C12' :
explanation.log_level === 'INFO' ? '#3498DB' : '#95A5A6',
color: 'white',
fontSize: '12px',
fontWeight: 'bold'
}}>
{explanation.log_level}
</span>
</div>
{explanation.timestamp && (
<div style={{ marginBottom: '15px', fontSize: '14px', color: '#666' }}>
<strong>Time:</strong> {explanation.timestamp}
</div>
)}
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
<strong>What this means:</strong>
<p style={{ margin: '5px 0' }}>{explanation.explanation}</p>
</div>
{explanation.action_needed && explanation.next_steps && explanation.next_steps.length > 0 && (
<div style={{ padding: '10px', backgroundColor: '#FEF5E7', borderRadius: '4px' }}>
<strong> Action needed:</strong>
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
{explanation.next_steps.map((step, i) => (
<li key={i} style={{ margin: '5px 0' }}>{step}</li>
))}
</ul>
</div>
)}
</div>
);
default:
return (
<div style={{ padding: '15px' }}>
<div>{explanation.explanation || 'No explanation available.'}</div>
</div>
);
}
};
return (
<>
<button
onClick={handleExplain}
onMouseEnter={(e) => e.target.style.backgroundColor = '#2980B9'}
onMouseLeave={(e) => e.target.style.backgroundColor = '#3498DB'}
style={buttonStyle}
title="Get AI-powered explanation"
>
<span></span>
<span>Explain</span>
</button>
{showModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
onClick={closeModal}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
maxWidth: '600px',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
position: 'relative'
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{
position: 'sticky',
top: 0,
backgroundColor: 'white',
padding: '15px',
borderBottom: '1px solid #ddd',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h2 style={{ margin: 0, fontSize: '20px' }}>Explanation</h2>
<button
onClick={closeModal}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#666'
}}
>
×
</button>
</div>
{renderExplanation()}
</div>
</div>
)}
</>
);
};
export default ExplainButton;

View File

@@ -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 (
<input
type={field.type}
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
style={commonStyle}
/>
);
case 'textarea':
return (
<textarea
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
placeholder={field.placeholder}
rows={4}
style={{ ...commonStyle, resize: 'vertical' }}
/>
);
case 'select':
return (
<select
value={formData[field.name] || ''}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
style={commonStyle}
>
<option value="">Select...</option>
{field.options?.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
);
case 'multiselect':
const selectedValues = formData[field.name] || [];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{field.options?.map(opt => (
<label key={opt.value} style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={selectedValues.includes(opt.value)}
onChange={(e) => {
const newValues = e.target.checked
? [...selectedValues, opt.value]
: selectedValues.filter(v => v !== opt.value);
handleFieldChange(field.name, newValues);
}}
/>
<span>{opt.label}</span>
</label>
))}
</div>
);
default:
return null;
}
};
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
width: '90%',
maxWidth: '700px',
maxHeight: '90vh',
overflow: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)'
}}
>
{/* Header */}
<div style={{
padding: '20px',
borderBottom: '2px solid #3498DB',
backgroundColor: '#f8f9fa'
}}>
<h2 style={{ margin: '0 0 10px 0', color: '#2C3E50' }}>{config.title}</h2>
{/* Progress indicator */}
<div style={{ display: 'flex', gap: '5px', marginTop: '15px' }}>
{config.steps.map((step, index) => (
<div
key={index}
style={{
flex: 1,
height: '4px',
backgroundColor: index + 1 <= currentStep ? '#3498DB' : '#ddd',
borderRadius: '2px',
transition: 'background-color 0.3s'
}}
/>
))}
</div>
<div style={{ marginTop: '8px', fontSize: '14px', color: '#666' }}>
Step {currentStep} of {totalSteps}
</div>
</div>
{/* Step content */}
<div style={{ padding: '30px' }}>
<h3 style={{ margin: '0 0 20px 0', color: '#34495E' }}>
{currentStepConfig.title}
</h3>
{/* Help section */}
{stepHelp && (
<div style={{
padding: '15px',
backgroundColor: '#E8F4F8',
borderRadius: '6px',
marginBottom: '20px',
borderLeft: '4px solid #3498DB'
}}>
{stepHelp.description && (
<p style={{ margin: '0 0 10px 0' }}>{stepHelp.description}</p>
)}
{stepHelp.tips && stepHelp.tips.length > 0 && (
<div>
<strong style={{ fontSize: '14px' }}>💡 Tips:</strong>
<ul style={{ margin: '5px 0 0 0', paddingLeft: '20px', fontSize: '14px' }}>
{stepHelp.tips.map((tip, i) => (
<li key={i} style={{ margin: '5px 0' }}>{tip}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Form fields */}
{currentStepConfig.fields.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
{currentStepConfig.fields.map(field => (
<div key={field.name}>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: '500',
color: '#2C3E50'
}}>
{field.label}
{field.required && <span style={{ color: '#E74C3C' }}> *</span>}
</label>
{renderField(field)}
</div>
))}
</div>
) : (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
<h4>Review Your Settings</h4>
<div style={{
marginTop: '20px',
textAlign: 'left',
backgroundColor: '#f8f9fa',
padding: '15px',
borderRadius: '4px',
maxHeight: '300px',
overflow: 'auto'
}}>
{Object.entries(formData).map(([key, value]) => (
<div key={key} style={{ marginBottom: '10px' }}>
<strong>{key}:</strong> {Array.isArray(value) ? value.join(', ') : value}
</div>
))}
</div>
</div>
)}
{/* Error message */}
{error && (
<div style={{
marginTop: '20px',
padding: '12px',
backgroundColor: '#FCE4E4',
color: '#E74C3C',
borderRadius: '4px',
fontSize: '14px'
}}>
{error}
</div>
)}
</div>
{/* Footer */}
<div style={{
padding: '20px',
borderTop: '1px solid #ddd',
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f8f9fa'
}}>
<button
onClick={onCancel}
style={{
padding: '10px 20px',
border: '1px solid #95A5A6',
backgroundColor: 'white',
color: '#666',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Cancel
</button>
<div style={{ display: 'flex', gap: '10px' }}>
{currentStep > 1 && (
<button
onClick={handleBack}
style={{
padding: '10px 20px',
border: '1px solid #3498DB',
backgroundColor: 'white',
color: '#3498DB',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Back
</button>
)}
<button
onClick={handleNext}
disabled={loading}
style={{
padding: '10px 20px',
border: 'none',
backgroundColor: loading ? '#95A5A6' : '#3498DB',
color: 'white',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{loading ? 'Processing...' : currentStep === totalSteps ? 'Finish' : 'Next'}
</button>
</div>
</div>
</div>
</div>
);
};
export default GuidedWizard;

View File

@@ -0,0 +1,424 @@
/**
* HelpChat Component
* Persistent side-panel chat with LLM-powered help
* Context-aware and maintains conversation history
*/
import React, { useState, useEffect, useRef } from 'react';
const HelpChat = ({
isOpen = false,
onClose,
currentPage = 'dashboard',
context = {}
}) => {
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [sessionId] = useState(() => `session-${Date.now()}`);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
if (isOpen && messages.length === 0) {
// Add welcome message
setMessages([{
role: 'assistant',
content: `👋 Hi! I'm your AI assistant for StrikePackageGPT. I can help you with:
• Understanding security tools and commands
• Interpreting scan results
• Writing nmap, nikto, and other tool commands
• Navigating the platform
• Security best practices
What would you like help with?`,
timestamp: new Date()
}]);
}
}, [isOpen]);
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus();
}
}, [isOpen]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleSendMessage = async () => {
if (!inputText.trim() || isLoading) return;
const userMessage = {
role: 'user',
content: inputText,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInputText('');
setIsLoading(true);
try {
// Build context string
const contextString = `User is on ${currentPage} page. ${JSON.stringify(context)}`;
const response = await fetch('/api/llm/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: inputText,
session_id: sessionId,
context: contextString
})
});
if (!response.ok) {
throw new Error('Failed to get response');
}
const data = await response.json();
const assistantMessage = {
role: 'assistant',
content: data.message || data.content || 'I apologize, I had trouble processing that request.',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Error sending message:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: '❌ Sorry, I encountered an error. Please try again.',
timestamp: new Date(),
isError: true
}]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
// Could show a toast notification here
console.log('Copied to clipboard');
});
};
const clearChat = () => {
if (window.confirm('Clear all chat history?')) {
setMessages([{
role: 'assistant',
content: 'Chat history cleared. How can I help you?',
timestamp: new Date()
}]);
}
};
const renderMessage = (message, index) => {
const isUser = message.role === 'user';
const isError = message.isError;
// Check if message contains code blocks
const hasCode = message.content.includes('```');
let renderedContent;
if (hasCode) {
// Simple code block rendering
const parts = message.content.split(/(```[\s\S]*?```)/g);
renderedContent = parts.map((part, i) => {
if (part.startsWith('```')) {
const code = part.slice(3, -3).trim();
const [lang, ...codeLines] = code.split('\n');
const codeText = codeLines.join('\n');
return (
<div key={i} style={{
backgroundColor: '#f5f5f5',
padding: '10px',
borderRadius: '4px',
margin: '10px 0',
position: 'relative',
fontFamily: 'monospace',
fontSize: '13px',
overflowX: 'auto'
}}>
<div style={{
fontSize: '11px',
color: '#666',
marginBottom: '5px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span>{lang || 'code'}</span>
<button
onClick={() => copyToClipboard(codeText)}
style={{
padding: '4px 8px',
fontSize: '11px',
border: 'none',
backgroundColor: '#ddd',
borderRadius: '3px',
cursor: 'pointer'
}}
>
📋 Copy
</button>
</div>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
<code>{codeText}</code>
</pre>
</div>
);
}
return <div key={i} style={{ whiteSpace: 'pre-wrap' }}>{part}</div>;
});
} else {
renderedContent = <div style={{ whiteSpace: 'pre-wrap' }}>{message.content}</div>;
}
return (
<div
key={index}
style={{
display: 'flex',
justifyContent: isUser ? 'flex-end' : 'flex-start',
marginBottom: '15px'
}}
>
<div
style={{
maxWidth: '80%',
padding: '12px 16px',
borderRadius: '12px',
backgroundColor: isError ? '#FCE4E4' : isUser ? '#3498DB' : '#ECF0F1',
color: isError ? '#E74C3C' : isUser ? 'white' : '#2C3E50',
fontSize: '14px',
lineHeight: '1.5',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}
>
{renderedContent}
<div
style={{
fontSize: '11px',
color: isUser ? 'rgba(255,255,255,0.7)' : '#95A5A6',
marginTop: '5px',
textAlign: 'right'
}}
>
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
);
};
const quickActions = [
{ label: '📝 Write nmap command', prompt: 'How do I write an nmap command to scan a network?' },
{ label: '🔍 Interpret results', prompt: 'Help me understand these scan results' },
{ label: '🛠️ Use sqlmap', prompt: 'How do I use sqlmap to test for SQL injection?' },
{ label: '📊 Generate report', prompt: 'How do I generate a security assessment report?' }
];
if (!isOpen) return null;
return (
<div
style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: '400px',
backgroundColor: 'white',
boxShadow: '-4px 0 15px rgba(0,0,0,0.1)',
display: 'flex',
flexDirection: 'column',
zIndex: 9998
}}
>
{/* Header */}
<div
style={{
padding: '20px',
backgroundColor: '#3498DB',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: '2px solid #2980B9'
}}
>
<div>
<h3 style={{ margin: '0 0 5px 0', fontSize: '18px' }}>💬 AI Assistant</h3>
<div style={{ fontSize: '12px', opacity: 0.9 }}>
Ask me anything about security testing
</div>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={clearChat}
style={{
background: 'none',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: '18px',
padding: '5px'
}}
title="Clear chat"
>
🗑
</button>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'white',
cursor: 'pointer',
fontSize: '24px',
padding: '0'
}}
title="Close"
>
×
</button>
</div>
</div>
{/* Messages area */}
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '20px',
backgroundColor: '#FAFAFA'
}}
>
{messages.map((message, index) => renderMessage(message, index))}
{isLoading && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#95A5A6' }}>
<div></div>
<div>Thinking...</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick actions */}
{messages.length <= 1 && (
<div
style={{
padding: '15px',
backgroundColor: '#F8F9FA',
borderTop: '1px solid #ddd'
}}
>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '10px' }}>
Quick actions:
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{quickActions.map((action, i) => (
<button
key={i}
onClick={() => {
setInputText(action.prompt);
inputRef.current?.focus();
}}
style={{
padding: '6px 12px',
fontSize: '12px',
border: '1px solid #ddd',
backgroundColor: 'white',
borderRadius: '16px',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
e.target.style.backgroundColor = '#E8F4F8';
e.target.style.borderColor = '#3498DB';
}}
onMouseLeave={(e) => {
e.target.style.backgroundColor = 'white';
e.target.style.borderColor = '#ddd';
}}
>
{action.label}
</button>
))}
</div>
</div>
)}
{/* Input area */}
<div
style={{
padding: '15px',
borderTop: '2px solid #ECF0F1',
backgroundColor: 'white'
}}
>
<div style={{ display: 'flex', gap: '10px' }}>
<textarea
ref={inputRef}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ask a question... (Enter to send)"
disabled={isLoading}
style={{
flex: 1,
padding: '10px',
border: '1px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
resize: 'none',
minHeight: '60px',
maxHeight: '120px',
fontFamily: 'inherit'
}}
/>
<button
onClick={handleSendMessage}
disabled={!inputText.trim() || isLoading}
style={{
padding: '10px 20px',
border: 'none',
backgroundColor: !inputText.trim() || isLoading ? '#95A5A6' : '#3498DB',
color: 'white',
borderRadius: '8px',
cursor: !inputText.trim() || isLoading ? 'not-allowed' : 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{isLoading ? '⏳' : '📤'}
</button>
</div>
<div style={{ fontSize: '11px', color: '#95A5A6', marginTop: '8px' }}>
Shift+Enter for new line
</div>
</div>
</div>
);
};
export default HelpChat;

View File

@@ -0,0 +1,315 @@
/**
* NetworkMap Component
* Interactive network graph visualization using Cytoscape.js
* Displays discovered hosts from nmap scans with OS/device icons
*/
import React, { useState, useEffect, useRef } from 'react';
const NetworkMap = ({ scanId, onNodeClick }) => {
const [hosts, setHosts] = useState([]);
const [loading, setLoading] = useState(false);
const [filterText, setFilterText] = useState('');
const cyRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
if (scanId) {
fetchHostData(scanId);
}
}, [scanId]);
useEffect(() => {
if (hosts.length > 0 && containerRef.current) {
initializeNetwork();
}
}, [hosts]);
const fetchHostData = async (scanId) => {
setLoading(true);
try {
const response = await fetch(`/api/nmap/hosts?scan_id=${scanId}`);
const data = await response.json();
setHosts(data.hosts || []);
} catch (error) {
console.error('Error fetching host data:', error);
} finally {
setLoading(false);
}
};
const initializeNetwork = () => {
// NOTE: This requires cytoscape.js to be installed: npm install cytoscape
// import cytoscape from 'cytoscape';
// Example initialization (requires actual cytoscape import)
/*
const cy = cytoscape({
container: containerRef.current,
elements: buildGraphElements(hosts),
style: getNetworkStyle(),
layout: {
name: 'cose',
idealEdgeLength: 100,
nodeOverlap: 20,
refresh: 20,
fit: true,
padding: 30,
randomize: false,
componentSpacing: 100,
nodeRepulsion: 400000,
edgeElasticity: 100,
nestingFactor: 5,
gravity: 80,
numIter: 1000,
initialTemp: 200,
coolingFactor: 0.95,
minTemp: 1.0
}
});
cy.on('tap', 'node', (evt) => {
const node = evt.target;
const hostData = node.data();
if (onNodeClick) {
onNodeClick(hostData);
}
});
cyRef.current = cy;
*/
};
const buildGraphElements = (hosts) => {
const elements = [];
// Add nodes for each host
hosts.forEach((host, index) => {
elements.push({
group: 'nodes',
data: {
id: `host-${index}`,
label: host.hostname || host.ip,
...host,
icon: getIconForHost(host)
},
classes: getNodeClass(host)
});
});
// Add edges (connections) - could be inferred from network topology
// For now, connect hosts in same subnet
const subnets = groupBySubnet(hosts);
Object.values(subnets).forEach(subnetHosts => {
if (subnetHosts.length > 1) {
for (let i = 0; i < subnetHosts.length - 1; i++) {
elements.push({
group: 'edges',
data: {
id: `edge-${subnetHosts[i].ip}-${subnetHosts[i + 1].ip}`,
source: `host-${hosts.indexOf(subnetHosts[i])}`,
target: `host-${hosts.indexOf(subnetHosts[i + 1])}`
}
});
}
}
});
return elements;
};
const getIconForHost = (host) => {
const osType = (host.os_type || '').toLowerCase();
const deviceType = (host.device_type || '').toLowerCase();
if (deviceType.includes('server')) return '/static/server.svg';
if (deviceType.includes('network') || deviceType.includes('router') || deviceType.includes('switch')) {
return '/static/network.svg';
}
if (deviceType.includes('workstation')) return '/static/workstation.svg';
if (osType.includes('windows')) return '/static/windows.svg';
if (osType.includes('linux') || osType.includes('unix')) return '/static/linux.svg';
if (osType.includes('mac') || osType.includes('darwin')) return '/static/mac.svg';
return '/static/unknown.svg';
};
const getNodeClass = (host) => {
const deviceType = (host.device_type || '').toLowerCase();
if (deviceType.includes('server')) return 'node-server';
if (deviceType.includes('network')) return 'node-network';
if (deviceType.includes('workstation')) return 'node-workstation';
return 'node-unknown';
};
const groupBySubnet = (hosts) => {
const subnets = {};
hosts.forEach(host => {
const subnet = host.ip.split('.').slice(0, 3).join('.');
if (!subnets[subnet]) {
subnets[subnet] = [];
}
subnets[subnet].push(host);
});
return subnets;
};
const getNetworkStyle = () => {
return [
{
selector: 'node',
style: {
'background-color': '#4A90E2',
'label': 'data(label)',
'text-valign': 'bottom',
'text-halign': 'center',
'font-size': '12px',
'color': '#333',
'text-margin-y': 5,
'width': 50,
'height': 50,
'background-image': 'data(icon)',
'background-fit': 'contain'
}
},
{
selector: '.node-server',
style: {
'background-color': '#4A90E2'
}
},
{
selector: '.node-network',
style: {
'background-color': '#16A085'
}
},
{
selector: '.node-workstation',
style: {
'background-color': '#5DADE2'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#95A5A6',
'curve-style': 'bezier'
}
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': '#E74C3C'
}
}
];
};
const exportToPNG = () => {
if (cyRef.current) {
const png = cyRef.current.png({ scale: 2, full: true });
const link = document.createElement('a');
link.href = png;
link.download = `network-map-${Date.now()}.png`;
link.click();
}
};
const exportToCSV = () => {
const csvContent = [
['IP', 'Hostname', 'OS Type', 'Device Type', 'MAC', 'Vendor', 'Open Ports'].join(','),
...hosts.map(host => [
host.ip,
host.hostname || '',
host.os_type || '',
host.device_type || '',
host.mac || '',
host.vendor || '',
(host.ports || []).map(p => p.port).join(';')
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `network-hosts-${Date.now()}.csv`;
link.click();
URL.revokeObjectURL(url);
};
const filteredHosts = hosts.filter(host => {
if (!filterText) return true;
const searchLower = filterText.toLowerCase();
return (
host.ip.includes(searchLower) ||
(host.hostname || '').toLowerCase().includes(searchLower) ||
(host.os_type || '').toLowerCase().includes(searchLower) ||
(host.device_type || '').toLowerCase().includes(searchLower)
);
});
return (
<div className="network-map-container" style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="network-map-toolbar" style={{ padding: '10px', borderBottom: '1px solid #ddd', display: 'flex', gap: '10px', alignItems: 'center' }}>
<input
type="text"
placeholder="Filter hosts (IP, hostname, OS, device type)..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
<button onClick={exportToPNG} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
Export PNG
</button>
<button onClick={exportToCSV} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
Export CSV
</button>
<span style={{ color: '#666' }}>
{filteredHosts.length} host{filteredHosts.length !== 1 ? 's' : ''}
</span>
</div>
<div
ref={containerRef}
className="network-map-canvas"
style={{
flex: 1,
backgroundColor: '#f5f5f5',
position: 'relative'
}}
>
{loading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center'
}}>
<div>Loading network map...</div>
</div>
)}
{!loading && hosts.length === 0 && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
color: '#666'
}}>
<div>No hosts discovered yet</div>
<div style={{ fontSize: '14px', marginTop: '10px' }}>Run a network scan to populate the map</div>
</div>
)}
</div>
</div>
);
};
export default NetworkMap;

View File

@@ -0,0 +1,354 @@
/**
* VoiceControls Component
* Microphone button with hotkey support for voice commands
* Visual feedback for listening, processing, and speaking states
*/
import React, { useState, useEffect, useRef } from 'react';
const VoiceControls = ({ onCommand, hotkey = ' ' }) => {
const [state, setState] = useState('idle'); // idle, listening, processing, speaking
const [transcript, setTranscript] = useState('');
const [error, setError] = useState(null);
const [permissionGranted, setPermissionGranted] = useState(false);
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const hotkeyPressedRef = useRef(false);
useEffect(() => {
// Check if browser supports MediaRecorder
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
setError('Voice control not supported in this browser');
return;
}
// Request microphone permission
requestMicrophonePermission();
// Setup hotkey listener
const handleKeyDown = (e) => {
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
hotkeyPressedRef.current = true;
startListening();
}
};
const handleKeyUp = (e) => {
if (e.key === hotkey && hotkeyPressedRef.current) {
hotkeyPressedRef.current = false;
if (state === 'listening') {
stopListening();
}
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
if (mediaRecorderRef.current && state === 'listening') {
mediaRecorderRef.current.stop();
}
};
}, [hotkey, state]);
const requestMicrophonePermission = async () => {
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
setPermissionGranted(true);
} catch (err) {
setError('Microphone permission denied');
setPermissionGranted(false);
}
};
const startListening = async () => {
if (!permissionGranted) {
await requestMicrophonePermission();
if (!permissionGranted) return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
audioChunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
await processAudio(audioBlob);
// Stop all tracks
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
setState('listening');
setTranscript('');
setError(null);
} catch (err) {
console.error('Error starting recording:', err);
setError('Failed to start recording: ' + err.message);
}
};
const stopListening = () => {
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
mediaRecorderRef.current.stop();
}
};
const processAudio = async (audioBlob) => {
setState('processing');
try {
// Send audio to backend for transcription
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
const response = await fetch('/api/voice/transcribe', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Transcription failed');
}
const data = await response.json();
const transcribedText = data.text || '';
setTranscript(transcribedText);
if (transcribedText) {
// Parse and route the voice command
await routeCommand(transcribedText);
} else {
setError('No speech detected');
setState('idle');
}
} catch (err) {
console.error('Error processing audio:', err);
setError('Failed to process audio: ' + err.message);
setState('idle');
}
};
const routeCommand = async (text) => {
try {
const response = await fetch('/api/voice/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error('Command routing failed');
}
const commandResult = await response.json();
// Call parent callback with command result
if (onCommand) {
onCommand(commandResult);
}
// Check if TTS response is available
if (commandResult.speak_response) {
await speakResponse(commandResult.speak_response);
} else {
setState('idle');
}
} catch (err) {
console.error('Error routing command:', err);
setError('Failed to execute command: ' + err.message);
setState('idle');
}
};
const speakResponse = async (text) => {
setState('speaking');
try {
// Try to get TTS audio from backend
const response = await fetch('/api/voice/speak', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (response.ok) {
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.onended = () => {
setState('idle');
URL.revokeObjectURL(audioUrl);
};
audio.play();
} else {
// Fallback to browser TTS
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.onend = () => setState('idle');
window.speechSynthesis.speak(utterance);
} else {
setState('idle');
}
}
} catch (err) {
console.error('Error speaking response:', err);
setState('idle');
}
};
const getStateColor = () => {
switch (state) {
case 'listening': return '#27AE60';
case 'processing': return '#F39C12';
case 'speaking': return '#3498DB';
default: return '#95A5A6';
}
};
const getStateIcon = () => {
switch (state) {
case 'listening': return '🎤';
case 'processing': return '⏳';
case 'speaking': return '🔊';
default: return '🎙️';
}
};
return (
<div
className="voice-controls"
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: 1000,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: '10px'
}}
>
{/* Transcript display */}
{transcript && (
<div
style={{
backgroundColor: 'white',
padding: '10px 15px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
maxWidth: '300px',
fontSize: '14px',
color: '#333'
}}
>
<strong>You said:</strong> {transcript}
</div>
)}
{/* Error display */}
{error && (
<div
style={{
backgroundColor: '#E74C3C',
color: 'white',
padding: '10px 15px',
borderRadius: '8px',
maxWidth: '300px',
fontSize: '14px'
}}
>
{error}
</div>
)}
{/* Mic button */}
<button
onClick={state === 'idle' ? startListening : stopListening}
disabled={state === 'processing' || state === 'speaking'}
style={{
width: '60px',
height: '60px',
borderRadius: '50%',
border: 'none',
backgroundColor: getStateColor(),
color: 'white',
fontSize: '24px',
cursor: state === 'idle' || state === 'listening' ? 'pointer' : 'not-allowed',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.3s ease',
transform: state === 'listening' ? 'scale(1.1)' : 'scale(1)',
opacity: state === 'processing' || state === 'speaking' ? 0.7 : 1
}}
title={`Voice command (hold ${hotkey === ' ' ? 'Space' : hotkey})`}
>
{getStateIcon()}
</button>
{/* Pulsing animation for listening state */}
{state === 'listening' && (
<div
style={{
position: 'absolute',
bottom: '0',
right: '0',
width: '60px',
height: '60px',
borderRadius: '50%',
border: '3px solid #27AE60',
animation: 'pulse 1.5s infinite',
pointerEvents: 'none'
}}
/>
)}
{/* Hotkey hint */}
<div
style={{
fontSize: '12px',
color: '#666',
textAlign: 'center'
}}
>
Hold {hotkey === ' ' ? 'Space' : hotkey} to talk
</div>
<style>{`
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.5;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
`}</style>
</div>
);
};
export default VoiceControls;

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#000000"/>
<ellipse cx="24" cy="20" rx="12" ry="14" fill="#FFFFFF"/>
<ellipse cx="24" cy="28" rx="10" ry="8" fill="#FDB515"/>
<circle cx="20" cy="18" r="2" fill="#000000"/>
<circle cx="28" cy="18" r="2" fill="#000000"/>
<path d="M 24 22 Q 22 24 24 24 Q 26 24 24 22" fill="none" stroke="#000000" stroke-width="1.5"/>
<path d="M 16 26 Q 18 30 24 32 Q 30 30 32 26" fill="none" stroke="#000000" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 554 B

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#A2AAAD"/>
<path d="M 30 14 Q 28 10 24 10 Q 20 10 18 14 Q 16 18 18 22 Q 20 26 24 26 Q 28 26 30 22 Q 32 18 30 14 Z" fill="#FFFFFF"/>
<circle cx="24" cy="16" r="4" fill="#A2AAAD"/>
<path d="M 26 10 Q 28 8 30 10" stroke="#FFFFFF" stroke-width="2" fill="none"/>
<rect x="22" y="26" width="4" height="8" fill="#FFFFFF"/>
<path d="M 18 34 Q 20 36 24 36 Q 28 36 30 34 L 28 32 Q 26 33 24 33 Q 22 33 20 32 Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 557 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#16A085" rx="2"/>
<rect x="6" y="18" width="36" height="12" rx="1" fill="#2C3E50"/>
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="18" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="22" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="26" cy="24" r="1.5" fill="#F39C12"/>
<circle cx="30" cy="24" r="1.5" fill="#95A5A6"/>
<circle cx="34" cy="24" r="1.5" fill="#95A5A6"/>
<circle cx="38" cy="24" r="1.5" fill="#95A5A6"/>
<line x1="24" y1="10" x2="24" y2="18" stroke="#ECF0F1" stroke-width="2"/>
<line x1="24" y1="30" x2="24" y2="38" stroke="#ECF0F1" stroke-width="2"/>
<line x1="10" y1="24" x2="4" y2="24" stroke="#ECF0F1" stroke-width="2"/>
<line x1="38" y1="24" x2="44" y2="24" stroke="#ECF0F1" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#4A90E2" rx="2"/>
<rect x="6" y="6" width="36" height="10" rx="1" fill="#2C3E50"/>
<rect x="6" y="19" width="36" height="10" rx="1" fill="#34495E"/>
<rect x="6" y="32" width="36" height="10" rx="1" fill="#2C3E50"/>
<circle cx="10" cy="11" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="11" r="1.5" fill="#F39C12"/>
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
<circle cx="10" cy="37" r="1.5" fill="#E74C3C"/>
<circle cx="14" cy="37" r="1.5" fill="#F39C12"/>
<rect x="18" y="9" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
<rect x="18" y="22" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
<rect x="18" y="35" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<circle cx="24" cy="24" r="22" fill="#95A5A6"/>
<circle cx="24" cy="24" r="18" fill="#7F8C8D"/>
<text x="24" y="32" font-size="24" font-weight="bold" fill="#ECF0F1" text-anchor="middle" font-family="Arial">?</text>
</svg>

After

Width:  |  Height:  |  Size: 312 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#0078D4" rx="2"/>
<rect x="6" y="6" width="17" height="17" fill="#FFFFFF"/>
<rect x="25" y="6" width="17" height="17" fill="#FFFFFF"/>
<rect x="6" y="25" width="17" height="17" fill="#FFFFFF"/>
<rect x="25" y="25" width="17" height="17" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
<rect width="48" height="48" fill="#5DADE2" rx="2"/>
<rect x="8" y="8" width="32" height="22" rx="1" fill="#34495E"/>
<rect x="10" y="10" width="28" height="18" fill="#3498DB"/>
<rect x="18" y="30" width="12" height="2" fill="#34495E"/>
<rect x="12" y="32" width="24" height="4" rx="1" fill="#2C3E50"/>
<circle cx="24" cy="14" r="2" fill="#ECF0F1"/>
<rect x="18" y="18" width="12" height="8" rx="1" fill="#ECF0F1"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

View File

@@ -0,0 +1,516 @@
"""
Configuration Validator Module
Validates configurations before save/change with plain-English warnings.
Provides backup/restore functionality and auto-fix suggestions.
"""
import json
import os
from typing import Dict, Any, List, Optional, Tuple
from datetime import datetime
import copy
# Configuration storage (in production, use persistent storage)
config_backups: Dict[str, List[Dict[str, Any]]] = {}
BACKUP_DIR = os.getenv("CONFIG_BACKUP_DIR", "/workspace/config_backups")
def validate_config(
config_data: Dict[str, Any],
config_type: str = "general"
) -> Dict[str, Any]:
"""
Validate configuration data before applying changes.
Args:
config_data: Configuration dictionary to validate
config_type: Type of configuration (scan, system, security, network)
Returns:
Dictionary with validation results
{
"valid": bool,
"warnings": List[str],
"errors": List[str],
"suggestions": List[Dict],
"safe_to_apply": bool
}
"""
errors = []
warnings = []
suggestions = []
# Type-specific validation
if config_type == "scan":
errors, warnings, suggestions = _validate_scan_config(config_data)
elif config_type == "network":
errors, warnings, suggestions = _validate_network_config(config_data)
elif config_type == "security":
errors, warnings, suggestions = _validate_security_config(config_data)
else:
errors, warnings, suggestions = _validate_general_config(config_data)
# Check for common issues across all config types
common_errors, common_warnings = _check_common_issues(config_data)
errors.extend(common_errors)
warnings.extend(common_warnings)
return {
"valid": len(errors) == 0,
"warnings": warnings,
"errors": errors,
"suggestions": suggestions,
"safe_to_apply": len(errors) == 0 and len([w for w in warnings if "critical" in w.lower()]) == 0,
"config_type": config_type
}
def _validate_scan_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
"""Validate scan configuration."""
errors = []
warnings = []
suggestions = []
# Check timeout
timeout = config_data.get("timeout", 300)
if not isinstance(timeout, (int, float)):
errors.append("Timeout must be a number (seconds)")
elif timeout < 1:
errors.append("Timeout must be at least 1 second")
elif timeout < 10:
warnings.append("Very short timeout (< 10s) may cause scans to fail prematurely")
elif timeout > 3600:
warnings.append("Very long timeout (> 1 hour) may cause scans to hang indefinitely")
# Check target
target = config_data.get("target", "")
if not target or not isinstance(target, str):
errors.append("Target must be specified (IP address, hostname, or network range)")
elif not _is_valid_target(target):
warnings.append(f"Target '{target}' may not be valid - ensure it's a valid IP, hostname, or CIDR")
# Check scan intensity
intensity = config_data.get("intensity", 3)
if isinstance(intensity, (int, float)):
if intensity < 1 or intensity > 5:
warnings.append("Scan intensity should be between 1 (stealth) and 5 (aggressive)")
if intensity >= 4:
warnings.append("High intensity scans may trigger IDS/IPS systems")
suggestions.append({
"field": "intensity",
"suggestion": 3,
"reason": "Balanced intensity for stealth and speed"
})
# Check port range
ports = config_data.get("ports", "")
if ports:
if not _is_valid_port_spec(str(ports)):
errors.append(f"Invalid port specification: {ports}")
return errors, warnings, suggestions
def _validate_network_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
"""Validate network configuration."""
errors = []
warnings = []
suggestions = []
# Check port
port = config_data.get("port")
if port is not None:
if not isinstance(port, int):
errors.append("Port must be an integer")
elif port < 1 or port > 65535:
errors.append("Port must be between 1 and 65535")
elif port < 1024:
warnings.append("Ports below 1024 require elevated privileges")
# Check host/bind address
host = config_data.get("host", "")
if host and not _is_valid_ip_or_hostname(host):
warnings.append(f"Host '{host}' may not be a valid IP address or hostname")
# Check max connections
max_conn = config_data.get("max_connections")
if max_conn is not None:
if not isinstance(max_conn, int) or max_conn < 1:
errors.append("max_connections must be a positive integer")
elif max_conn > 1000:
warnings.append("Very high max_connections (> 1000) may exhaust system resources")
return errors, warnings, suggestions
def _validate_security_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
"""Validate security configuration."""
errors = []
warnings = []
suggestions = []
# Check for exposed secrets
for key, value in config_data.items():
if any(secret_word in key.lower() for secret_word in ['password', 'secret', 'token', 'key', 'credential']):
if isinstance(value, str):
if len(value) < 8:
warnings.append(f"SECURITY: {key} appears weak (< 8 characters)")
if value in ['password', '123456', 'admin', 'default']:
errors.append(f"SECURITY: {key} is using a default/weak value")
# Check SSL/TLS settings
ssl_enabled = config_data.get("ssl_enabled", False)
if not ssl_enabled:
warnings.append("SECURITY: SSL/TLS is disabled - data will be transmitted unencrypted")
# Check authentication
auth_enabled = config_data.get("authentication_enabled", True)
if not auth_enabled:
warnings.append("SECURITY: Authentication is disabled - system will be exposed")
return errors, warnings, suggestions
def _validate_general_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
"""Validate general configuration."""
errors = []
warnings = []
suggestions = []
# Check for valid JSON structure
if not isinstance(config_data, dict):
errors.append("Configuration must be a JSON object")
return errors, warnings, suggestions
# Check for empty config
if not config_data:
warnings.append("Configuration is empty")
return errors, warnings, suggestions
def _check_common_issues(config_data: Dict[str, Any]) -> Tuple[List[str], List[str]]:
"""Check for common configuration issues."""
errors = []
warnings = []
# Check for null/undefined values
for key, value in config_data.items():
if value is None:
warnings.append(f"Value for '{key}' is null - will use default")
# Check for suspicious paths
for key, value in config_data.items():
if isinstance(value, str):
if value.startswith('/root/') or value.startswith('C:\\Windows\\'):
warnings.append(f"SECURITY: '{key}' points to a sensitive system path")
return errors, warnings
def backup_config(config_name: str, config_data: Dict[str, Any], description: str = "") -> Dict[str, Any]:
"""
Create a backup of current configuration.
Args:
config_name: Name/ID of the configuration
config_data: Configuration data to backup
description: Optional description of the backup
Returns:
Dictionary with backup information
"""
timestamp = datetime.utcnow().isoformat()
backup_id = f"{config_name}_{timestamp}"
backup = {
"backup_id": backup_id,
"config_name": config_name,
"timestamp": timestamp,
"description": description or "Automatic backup",
"config_data": copy.deepcopy(config_data),
"size_bytes": len(json.dumps(config_data))
}
# Store in memory
if config_name not in config_backups:
config_backups[config_name] = []
config_backups[config_name].append(backup)
# Keep only last 10 backups per config
if len(config_backups[config_name]) > 10:
config_backups[config_name] = config_backups[config_name][-10:]
# Also save to disk if backup directory exists
try:
os.makedirs(BACKUP_DIR, exist_ok=True)
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
with open(backup_file, 'w') as f:
json.dump(backup, f, indent=2)
except Exception as e:
print(f"Warning: Could not save backup to disk: {e}")
return {
"success": True,
"backup_id": backup_id,
"timestamp": timestamp,
"message": f"Configuration backed up successfully"
}
def restore_config(backup_id: str) -> Dict[str, Any]:
"""
Restore configuration from a backup.
Args:
backup_id: ID of the backup to restore
Returns:
Dictionary with restored configuration and metadata
"""
# Search in memory backups
for config_name, backups in config_backups.items():
for backup in backups:
if backup["backup_id"] == backup_id:
return {
"success": True,
"backup_id": backup_id,
"config_name": config_name,
"config_data": copy.deepcopy(backup["config_data"]),
"timestamp": backup["timestamp"],
"description": backup["description"],
"message": "Configuration restored successfully"
}
# Try loading from disk
try:
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
if os.path.exists(backup_file):
with open(backup_file, 'r') as f:
backup = json.load(f)
return {
"success": True,
"backup_id": backup_id,
"config_name": backup["config_name"],
"config_data": backup["config_data"],
"timestamp": backup["timestamp"],
"description": backup.get("description", ""),
"message": "Configuration restored from disk backup"
}
except Exception as e:
pass
return {
"success": False,
"backup_id": backup_id,
"error": "Backup not found",
"message": f"No backup found with ID: {backup_id}"
}
def suggest_autofix(validation_result: Dict[str, Any], config_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Suggest automatic fixes for configuration issues.
Args:
validation_result: Result from validate_config()
config_data: Original configuration data
Returns:
Dictionary with auto-fix suggestions
"""
if validation_result.get("valid") and not validation_result.get("warnings"):
return {
"has_fixes": False,
"message": "Configuration is valid, no fixes needed"
}
fixed_config = copy.deepcopy(config_data)
fixes_applied = []
# Apply suggestions from validation
for suggestion in validation_result.get("suggestions", []):
field = suggestion.get("field")
suggested_value = suggestion.get("suggestion")
reason = suggestion.get("reason")
if field in fixed_config:
old_value = fixed_config[field]
fixed_config[field] = suggested_value
fixes_applied.append({
"field": field,
"old_value": old_value,
"new_value": suggested_value,
"reason": reason
})
# Apply common fixes based on errors
for error in validation_result.get("errors", []):
if "timeout must be" in error.lower():
if "timeout" in fixed_config:
fixed_config["timeout"] = 300 # Default safe timeout
fixes_applied.append({
"field": "timeout",
"old_value": config_data.get("timeout"),
"new_value": 300,
"reason": "Reset to safe default value"
})
if "port must be" in error.lower():
if "port" in fixed_config:
fixed_config["port"] = 8080 # Default safe port
fixes_applied.append({
"field": "port",
"old_value": config_data.get("port"),
"new_value": 8080,
"reason": "Reset to safe default port"
})
return {
"has_fixes": len(fixes_applied) > 0,
"fixes_applied": fixes_applied,
"fixed_config": fixed_config,
"message": f"Applied {len(fixes_applied)} automatic fixes"
}
def list_backups(config_name: Optional[str] = None) -> Dict[str, Any]:
"""
List available configuration backups.
Args:
config_name: Optional config name to filter by
Returns:
Dictionary with list of backups
"""
all_backups = []
# Get from memory
if config_name:
backups = config_backups.get(config_name, [])
for backup in backups:
all_backups.append({
"backup_id": backup["backup_id"],
"config_name": backup["config_name"],
"timestamp": backup["timestamp"],
"description": backup["description"],
"size_bytes": backup["size_bytes"]
})
else:
for cfg_name, backups in config_backups.items():
for backup in backups:
all_backups.append({
"backup_id": backup["backup_id"],
"config_name": backup["config_name"],
"timestamp": backup["timestamp"],
"description": backup["description"],
"size_bytes": backup["size_bytes"]
})
# Also check disk backups
try:
if os.path.exists(BACKUP_DIR):
for filename in os.listdir(BACKUP_DIR):
if filename.endswith('.json'):
backup_id = filename[:-5] # Remove .json
# Check if already in list (avoid duplicates)
if not any(b["backup_id"] == backup_id for b in all_backups):
try:
filepath = os.path.join(BACKUP_DIR, filename)
with open(filepath, 'r') as f:
backup = json.load(f)
if not config_name or backup["config_name"] == config_name:
all_backups.append({
"backup_id": backup["backup_id"],
"config_name": backup["config_name"],
"timestamp": backup["timestamp"],
"description": backup.get("description", ""),
"size_bytes": os.path.getsize(filepath)
})
except:
pass
except Exception as e:
print(f"Warning: Could not read disk backups: {e}")
# Sort by timestamp (newest first)
all_backups.sort(key=lambda x: x["timestamp"], reverse=True)
return {
"backups": all_backups,
"count": len(all_backups),
"config_name": config_name
}
# Validation helper functions
def _is_valid_target(target: str) -> bool:
"""Check if target is a valid IP, hostname, or CIDR."""
import re
# IP address
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if re.match(ip_pattern, target):
parts = target.split('.')
return all(0 <= int(part) <= 255 for part in parts)
# CIDR notation
if '/' in target:
cidr_pattern = r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
if re.match(cidr_pattern, target):
ip_part = target.split('/')[0]
return _is_valid_target(ip_part)
# IP range
if '-' in target:
range_pattern = r'^(\d{1,3}\.){3}\d{1,3}-\d{1,3}$'
if re.match(range_pattern, target):
return True
# Hostname/domain
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
if re.match(hostname_pattern, target):
return True
return False
def _is_valid_port_spec(ports: str) -> bool:
"""Check if port specification is valid."""
import re
# Single port
if ports.isdigit():
port_num = int(ports)
return 1 <= port_num <= 65535
# Port range
if '-' in ports:
range_pattern = r'^\d+-\d+$'
if re.match(range_pattern, ports):
start, end = map(int, ports.split('-'))
return 1 <= start <= end <= 65535
# Comma-separated ports
if ',' in ports:
port_list = ports.split(',')
return all(_is_valid_port_spec(p.strip()) for p in port_list)
return False
def _is_valid_ip_or_hostname(host: str) -> bool:
"""Check if host is a valid IP address or hostname."""
import re
# IP address
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if re.match(ip_pattern, host):
parts = host.split('.')
return all(0 <= int(part) <= 255 for part in parts)
# Hostname
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
return bool(re.match(hostname_pattern, host))

View File

@@ -0,0 +1,547 @@
"""
Explain Module
Provides "Explain this" functionality for configs, logs, errors, and onboarding.
Generates plain-English explanations and suggestions for fixes.
"""
from typing import Dict, Any, Optional, List
import re
import os
def explain_config(config_key: str, config_value: Any, context: Optional[Dict] = None) -> Dict[str, Any]:
"""
Explain a configuration setting in plain English.
Args:
config_key: Configuration key/name
config_value: Current value of the configuration
context: Additional context about the configuration
Returns:
Dictionary with explanation and recommendations
"""
# Common configuration patterns and their explanations
config_patterns = {
r'.*timeout.*': {
'description': 'Controls how long the system waits before giving up on an operation',
'example': 'A timeout of 30 seconds means operations will be cancelled after 30s',
'recommendations': [
'Increase timeout for slow networks or large scans',
'Decrease timeout for faster detection of unavailable services',
'Typical values: 10-300 seconds'
]
},
r'.*port.*': {
'description': 'Specifies which network port to use for communication',
'example': 'Port 8080 is commonly used for web applications',
'recommendations': [
'Use standard ports (80/443) for production',
'Use high ports (8000+) for development',
'Ensure port is not blocked by firewall'
]
},
r'.*api[_-]?key.*': {
'description': 'Authentication key for accessing external services',
'example': 'API keys should be kept secret and not shared publicly',
'recommendations': [
'Store API keys in environment variables',
'Never commit API keys to version control',
'Rotate keys regularly for security'
]
},
r'.*thread.*|.*worker.*': {
'description': 'Controls parallel processing and concurrency',
'example': '4 workers means 4 operations can run simultaneously',
'recommendations': [
'More workers = faster but more resource usage',
'Typical range: number of CPU cores or 2x CPU cores',
'Too many workers can overwhelm the system'
]
},
r'.*rate[_-]?limit.*': {
'description': 'Limits the frequency of operations to prevent overload',
'example': 'Rate limit of 100/minute means max 100 requests per minute',
'recommendations': [
'Set based on target system capabilities',
'Lower for sensitive or production targets',
'Higher for testing environments'
]
}
}
# Find matching pattern
explanation = {
'description': 'Configuration setting',
'example': '',
'recommendations': []
}
for pattern, details in config_patterns.items():
if re.search(pattern, config_key, re.IGNORECASE):
explanation = details
break
# Value-specific analysis
value_analysis = _analyze_config_value(config_key, config_value)
return {
'config_key': config_key,
'current_value': str(config_value),
'description': explanation['description'],
'example': explanation['example'],
'recommendations': explanation['recommendations'],
'value_analysis': value_analysis,
'safe_to_change': _is_safe_to_change(config_key),
'requires_restart': _requires_restart(config_key)
}
def explain_error(error_message: str, error_type: Optional[str] = None, context: Optional[Dict] = None) -> Dict[str, Any]:
"""
Explain an error message in plain English with suggested fixes.
Args:
error_message: The error message text
error_type: Type/category of error (if known)
context: Additional context about where/when the error occurred
Returns:
Dictionary with explanation and fix suggestions
"""
# Common error patterns
error_patterns = [
{
'pattern': r'connection\s+(refused|timed?\s?out|failed|reset)',
'plain_english': 'Unable to connect to the target',
'likely_causes': [
'Target is offline or unreachable',
'Firewall blocking the connection',
'Wrong IP address or port',
'Network connectivity issues'
],
'suggested_fixes': [
'Verify target IP address is correct',
'Check if target is online (ping test)',
'Ensure no firewall is blocking the connection',
'Try a different port or protocol'
]
},
{
'pattern': r'permission\s+denied|access\s+denied|forbidden',
'plain_english': 'You don\'t have permission to perform this action',
'likely_causes': [
'Insufficient user privileges',
'Authentication failed',
'Resource is protected',
'Rate limiting in effect'
],
'suggested_fixes': [
'Run with appropriate privileges (sudo if needed)',
'Check authentication credentials',
'Verify you have permission to access this resource',
'Wait before retrying (if rate limited)'
]
},
{
'pattern': r'not\s+found|does\s+not\s+exist|no\s+such',
'plain_english': 'The requested resource could not be found',
'likely_causes': [
'Resource has been moved or deleted',
'Incorrect path or name',
'Typo in the request',
'Resource not yet created'
],
'suggested_fixes': [
'Check spelling and capitalization',
'Verify the resource exists',
'Check if path or URL is correct',
'Create the resource if needed'
]
},
{
'pattern': r'invalid\s+(argument|parameter|input|syntax)',
'plain_english': 'The input provided is not valid or in the wrong format',
'likely_causes': [
'Wrong data type or format',
'Missing required parameter',
'Value out of valid range',
'Syntax error in command'
],
'suggested_fixes': [
'Check documentation for correct format',
'Verify all required parameters are provided',
'Ensure values are within valid ranges',
'Check for typos in the command'
]
},
{
'pattern': r'timeout|timed\s+out',
'plain_english': 'The operation took too long and was cancelled',
'likely_causes': [
'Network is slow or congested',
'Target is responding slowly',
'Timeout setting is too low',
'Large operation needs more time'
],
'suggested_fixes': [
'Increase timeout value in settings',
'Check network connectivity',
'Try again during off-peak hours',
'Break operation into smaller parts'
]
},
{
'pattern': r'out\s+of\s+memory|memory\s+error',
'plain_english': 'The system ran out of available memory',
'likely_causes': [
'Too many concurrent operations',
'Processing too much data at once',
'Memory leak in the application',
'Insufficient system resources'
],
'suggested_fixes': [
'Reduce number of concurrent operations',
'Process data in smaller batches',
'Restart the application',
'Add more RAM to the system'
]
}
]
# Find matching pattern
match_result = {
'plain_english': 'An error occurred',
'likely_causes': ['Unknown error condition'],
'suggested_fixes': ['Check logs for more details', 'Try the operation again']
}
error_lower = error_message.lower()
for pattern_info in error_patterns:
if re.search(pattern_info['pattern'], error_lower):
match_result = {
'plain_english': pattern_info['plain_english'],
'likely_causes': pattern_info['likely_causes'],
'suggested_fixes': pattern_info['suggested_fixes']
}
break
return {
'original_error': error_message,
'error_type': error_type or 'unknown',
'plain_english': match_result['plain_english'],
'likely_causes': match_result['likely_causes'],
'suggested_fixes': match_result['suggested_fixes'],
'severity': _assess_error_severity(error_message),
'context': context or {}
}
def explain_log_entry(log_entry: str, log_level: Optional[str] = None) -> Dict[str, Any]:
"""
Explain a log entry in plain English.
Args:
log_entry: The log message text
log_level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Returns:
Dictionary with explanation of the log entry
"""
# Detect log level if not provided
if not log_level:
log_level = _detect_log_level(log_entry)
# Extract key information from log
extracted_info = _extract_log_info(log_entry)
# Determine if action is needed
action_needed = log_level in ['ERROR', 'CRITICAL', 'WARNING']
explanation = {
'log_entry': log_entry,
'log_level': log_level,
'timestamp': extracted_info.get('timestamp'),
'component': extracted_info.get('component'),
'message': extracted_info.get('message', log_entry),
'action_needed': action_needed,
'explanation': _generate_log_explanation(log_entry, log_level),
'next_steps': _suggest_log_next_steps(log_entry, log_level) if action_needed else []
}
return explanation
def get_wizard_step_help(wizard_type: str, step_number: int) -> Dict[str, Any]:
"""
Get help text for a specific wizard step.
Args:
wizard_type: Type of wizard (create_operation, onboard_agent, run_scan, first_time_setup)
step_number: Current step number (1-indexed)
Returns:
Dictionary with help information for the step
"""
wizard_help = {
'create_operation': {
1: {
'title': 'Operation Name and Type',
'description': 'Give your operation a memorable name and select the type of security assessment',
'tips': [
'Use descriptive names like "Q4 External Assessment" or "Web App Pentest"',
'Choose the operation type that matches your goals',
'You can change these later in settings'
],
'example': 'Example: "Internal Network Audit - Production"'
},
2: {
'title': 'Define Target Scope',
'description': 'Specify which systems, networks, or applications to include in the assessment',
'tips': [
'Use CIDR notation for network ranges (e.g., 192.168.1.0/24)',
'Add individual hosts or domains as needed',
'Clearly define what is in-scope and out-of-scope'
],
'example': 'Example: 192.168.1.0/24, app.example.com'
},
3: {
'title': 'Configure Assessment Tools',
'description': 'Select which security tools to use and configure their settings',
'tips': [
'Start with reconnaissance tools (nmap, whatweb)',
'Add vulnerability scanners based on target type',
'Adjust scan intensity based on target sensitivity'
],
'example': 'Example: nmap (aggressive), nikto (web servers only)'
}
},
'run_scan': {
1: {
'title': 'Select Scan Tool',
'description': 'Choose the security tool appropriate for your target',
'tips': [
'nmap: Network scanning and service detection',
'nikto: Web server vulnerability scanning',
'gobuster: Directory and file discovery',
'sqlmap: SQL injection testing'
],
'example': 'For a web server, use nikto or gobuster'
},
2: {
'title': 'Specify Target',
'description': 'Enter the IP address, hostname, or network range to scan',
'tips': [
'Single host: 192.168.1.100 or example.com',
'Network range: 192.168.1.0/24',
'Multiple hosts: 192.168.1.1-50'
],
'example': 'Example: 192.168.1.0/24 for entire subnet'
},
3: {
'title': 'Scan Options',
'description': 'Configure scan parameters and intensity',
'tips': [
'Quick scan: Fast but less thorough',
'Full scan: Comprehensive but slower',
'Stealth: Slower but harder to detect'
],
'example': 'Use quick scan for initial reconnaissance'
}
}
}
steps = wizard_help.get(wizard_type, {})
step_help = steps.get(step_number, {
'title': f'Step {step_number}',
'description': 'Complete this step to continue',
'tips': ['Fill in the required information'],
'example': ''
})
return {
'wizard_type': wizard_type,
'step_number': step_number,
'total_steps': len(steps),
**step_help
}
def suggest_fix(issue_description: str, context: Optional[Dict] = None) -> List[str]:
"""
Suggest fixes for a described issue.
Args:
issue_description: Description of the problem
context: Additional context (error codes, logs, etc.)
Returns:
List of suggested fix actions
"""
issue_lower = issue_description.lower()
fixes = []
# Connectivity issues
if any(word in issue_lower for word in ['connect', 'network', 'reach', 'timeout']):
fixes.extend([
'Verify target is online with ping test',
'Check firewall rules and network connectivity',
'Ensure correct IP address and port number',
'Try increasing timeout value in settings'
])
# Permission issues
if any(word in issue_lower for word in ['permission', 'access', 'denied', 'forbidden']):
fixes.extend([
'Run with elevated privileges (sudo)',
'Check file/directory permissions',
'Verify authentication credentials',
'Ensure user has required roles/permissions'
])
# Configuration issues
if any(word in issue_lower for word in ['config', 'setting', 'option']):
fixes.extend([
'Review configuration file for errors',
'Restore default configuration',
'Check configuration documentation',
'Validate configuration format (JSON/YAML)'
])
# Tool/command issues
if any(word in issue_lower for word in ['command', 'tool', 'not found', 'install']):
fixes.extend([
'Install the required tool or package',
'Check if tool is in system PATH',
'Verify tool name spelling',
'Update tool to latest version'
])
# Default suggestions if no specific fix found
if not fixes:
fixes = [
'Check system logs for more details',
'Restart the affected service',
'Review recent configuration changes',
'Consult documentation or support'
]
return fixes[:5] # Return top 5 suggestions
# Helper functions
def _analyze_config_value(key: str, value: Any) -> str:
"""Analyze a configuration value and provide feedback."""
if isinstance(value, int):
if 'timeout' in key.lower():
if value < 10:
return 'Very low - may cause premature failures'
elif value > 300:
return 'Very high - operations may take long to fail'
else:
return 'Reasonable value'
elif 'port' in key.lower():
if value < 1024:
return 'System port - requires elevated privileges'
else:
return 'User port - no special privileges needed'
return 'Current value seems valid'
def _is_safe_to_change(config_key: str) -> bool:
"""Determine if a config is safe to change without risk."""
unsafe_keys = ['database', 'credential', 'key', 'secret', 'password']
return not any(unsafe in config_key.lower() for unsafe in unsafe_keys)
def _requires_restart(config_key: str) -> bool:
"""Determine if changing this config requires a restart."""
restart_keys = ['port', 'host', 'database', 'worker', 'thread']
return any(key in config_key.lower() for key in restart_keys)
def _assess_error_severity(error_message: str) -> str:
"""Assess the severity of an error."""
error_lower = error_message.lower()
if any(word in error_lower for word in ['critical', 'fatal', 'crash', 'panic']):
return 'critical'
elif any(word in error_lower for word in ['error', 'fail', 'exception']):
return 'high'
elif any(word in error_lower for word in ['warning', 'warn']):
return 'medium'
else:
return 'low'
def _detect_log_level(log_entry: str) -> str:
"""Detect log level from log entry."""
levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
for level in levels:
if level in log_entry.upper():
return level
return 'INFO'
def _extract_log_info(log_entry: str) -> Dict[str, str]:
"""Extract structured information from a log entry."""
info = {}
# Try to extract timestamp
timestamp_pattern = r'\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}'
timestamp_match = re.search(timestamp_pattern, log_entry)
if timestamp_match:
info['timestamp'] = timestamp_match.group()
# Try to extract component/module name
component_pattern = r'\[(\w+)\]'
component_match = re.search(component_pattern, log_entry)
if component_match:
info['component'] = component_match.group(1)
# Extract the main message
parts = log_entry.split(':', 1)
if len(parts) > 1:
info['message'] = parts[1].strip()
else:
info['message'] = log_entry
return info
def _generate_log_explanation(log_entry: str, log_level: str) -> str:
"""Generate a plain English explanation of a log entry."""
if log_level == 'ERROR':
return 'An error occurred that may require attention. Check the details to understand what went wrong.'
elif log_level == 'WARNING':
return 'A potential issue was detected. It may not be critical but should be reviewed.'
elif log_level == 'INFO':
return 'Normal operational message providing status information.'
elif log_level == 'DEBUG':
return 'Detailed diagnostic information useful for troubleshooting.'
else:
return 'Log entry documenting system activity.'
def _suggest_log_next_steps(log_entry: str, log_level: str) -> List[str]:
"""Suggest next steps based on log entry."""
steps = []
if log_level in ['ERROR', 'CRITICAL']:
steps.append('Review the error details and check related logs')
steps.append('Check if the issue is repeating or isolated')
steps.append('Consider rolling back recent changes if applicable')
if log_level == 'WARNING':
steps.append('Monitor for repeated warnings')
steps.append('Check if this indicates a trend or pattern')
if 'connection' in log_entry.lower():
steps.append('Verify network connectivity to the target')
if 'timeout' in log_entry.lower():
steps.append('Consider increasing timeout values')
return steps

View File

@@ -0,0 +1,435 @@
"""
LLM Help Module
Provides LLM-powered assistance including chat help, autocomplete, and config suggestions.
Maintains conversation context for persistent help sessions.
"""
from typing import Dict, Any, List, Optional
import os
import httpx
import json
# Store conversation history per session
conversation_contexts: Dict[str, List[Dict[str, str]]] = {}
async def chat_completion(
message: str,
session_id: Optional[str] = None,
context: Optional[str] = None,
provider: str = "ollama",
model: str = "llama3.2",
system_prompt: Optional[str] = None
) -> Dict[str, Any]:
"""
Get LLM chat completion with context awareness.
Args:
message: User message
session_id: Session ID for maintaining conversation context
context: Additional context about current page/operation
provider: LLM provider (ollama, openai, anthropic)
model: Model name
system_prompt: Custom system prompt (uses default if not provided)
Returns:
Dictionary with LLM response and metadata
"""
# Default system prompt for help
if not system_prompt:
system_prompt = """You are a helpful AI assistant for StrikePackageGPT, a security testing platform.
You help users with:
- Understanding security tools and concepts
- Writing and understanding nmap, nikto, and other security tool commands
- Interpreting scan results and vulnerabilities
- Best practices for penetration testing
- Navigation and usage of the platform
Provide clear, concise, and actionable advice. Include command examples when relevant.
Always emphasize ethical hacking practices and legal considerations."""
# Build messages with conversation history
messages = [{"role": "system", "content": system_prompt}]
# Add conversation history if session_id provided
if session_id and session_id in conversation_contexts:
messages.extend(conversation_contexts[session_id][-10:]) # Last 10 messages
# Add context if provided
if context:
messages.append({"role": "system", "content": f"Current context: {context}"})
# Add user message
messages.append({"role": "user", "content": message})
# Get LLM response
try:
llm_router_url = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
async with httpx.AsyncClient() as client:
response = await client.post(
f"{llm_router_url}/chat",
json={
"provider": provider,
"model": model,
"messages": messages,
"temperature": 0.7,
"max_tokens": 2048
},
timeout=120.0
)
if response.status_code == 200:
result = response.json()
assistant_message = result.get("content", "")
# Store in conversation history
if session_id:
if session_id not in conversation_contexts:
conversation_contexts[session_id] = []
conversation_contexts[session_id].append({"role": "user", "content": message})
conversation_contexts[session_id].append({"role": "assistant", "content": assistant_message})
return {
"message": assistant_message,
"session_id": session_id,
"provider": provider,
"model": model,
"success": True
}
else:
return {
"message": "I'm having trouble connecting to the LLM service. Please try again.",
"error": response.text,
"success": False
}
except httpx.ConnectError:
return {
"message": "LLM service is not available. Please check your connection.",
"error": "Connection failed",
"success": False
}
except Exception as e:
return {
"message": "An error occurred while processing your request.",
"error": str(e),
"success": False
}
async def get_autocomplete(
partial_text: str,
context_type: str = "command",
max_suggestions: int = 5
) -> List[Dict[str, str]]:
"""
Get autocomplete suggestions for commands or configurations.
Args:
partial_text: Partial text entered by user
context_type: Type of autocomplete (command, config, target)
max_suggestions: Maximum number of suggestions to return
Returns:
List of suggestion dictionaries with text and description
"""
suggestions = []
if context_type == "command":
suggestions = _get_command_suggestions(partial_text)
elif context_type == "config":
suggestions = _get_config_suggestions(partial_text)
elif context_type == "target":
suggestions = _get_target_suggestions(partial_text)
return suggestions[:max_suggestions]
def _get_command_suggestions(partial_text: str) -> List[Dict[str, str]]:
"""Get command autocomplete suggestions."""
# Common security tool commands
commands = [
{"text": "nmap -sV -sC", "description": "Service version detection with default scripts"},
{"text": "nmap -p- -T4", "description": "Scan all ports with aggressive timing"},
{"text": "nmap -sS -O", "description": "SYN stealth scan with OS detection"},
{"text": "nmap --script vuln", "description": "Run vulnerability detection scripts"},
{"text": "nikto -h", "description": "Web server vulnerability scan"},
{"text": "gobuster dir -u", "description": "Directory brute-forcing"},
{"text": "sqlmap -u", "description": "SQL injection testing"},
{"text": "whatweb", "description": "Web technology fingerprinting"},
{"text": "searchsploit", "description": "Search exploit database"},
{"text": "hydra -l", "description": "Network login cracking"}
]
# Filter based on partial text
partial_lower = partial_text.lower()
return [cmd for cmd in commands if cmd["text"].lower().startswith(partial_lower)]
def _get_config_suggestions(partial_text: str) -> List[Dict[str, str]]:
"""Get configuration autocomplete suggestions."""
configs = [
{"text": "timeout", "description": "Command execution timeout in seconds"},
{"text": "max_workers", "description": "Maximum parallel workers"},
{"text": "scan_intensity", "description": "Scan aggressiveness (1-5)"},
{"text": "rate_limit", "description": "Requests per second limit"},
{"text": "default_ports", "description": "Default ports to scan"},
{"text": "output_format", "description": "Output format (json, xml, text)"},
{"text": "log_level", "description": "Logging verbosity (debug, info, warning, error)"},
{"text": "retry_count", "description": "Number of retries on failure"}
]
partial_lower = partial_text.lower()
return [cfg for cfg in configs if cfg["text"].lower().startswith(partial_lower)]
def _get_target_suggestions(partial_text: str) -> List[Dict[str, str]]:
"""Get target specification autocomplete suggestions."""
suggestions = [
{"text": "192.168.1.0/24", "description": "Scan entire /24 subnet"},
{"text": "192.168.1.1-50", "description": "Scan IP range"},
{"text": "10.0.0.0/8", "description": "Scan entire /8 network"},
{"text": "localhost", "description": "Scan local machine"},
{"text": "example.com", "description": "Scan domain name"}
]
return suggestions
async def explain_anything(
item: str,
item_type: str = "auto",
context: Optional[Dict] = None
) -> Dict[str, Any]:
"""
Explain anything using LLM - commands, configs, errors, concepts.
Args:
item: The item to explain
item_type: Type of item (auto, command, config, error, concept)
context: Additional context
Returns:
Dictionary with explanation
"""
# Auto-detect type if not specified
if item_type == "auto":
item_type = _detect_item_type(item)
# Build appropriate prompt based on type
prompts = {
"command": f"Explain this security command in plain English:\n{item}\n\nInclude: what it does, any flags/options, expected output, and safety considerations.",
"config": f"Explain this configuration setting:\n{item}\n\nInclude: purpose, typical values, and recommendations.",
"error": f"Explain this error message:\n{item}\n\nInclude: what went wrong, likely causes, and how to fix it.",
"concept": f"Explain this security concept:\n{item}\n\nProvide a clear, beginner-friendly explanation with examples.",
"scan_result": f"Explain this scan result:\n{item}\n\nInclude: significance, risk level, and recommended actions."
}
prompt = prompts.get(item_type, f"Explain: {item}")
# Get explanation from LLM
result = await chat_completion(
message=prompt,
system_prompt="You are a security education assistant. Provide clear, concise explanations suitable for both beginners and experts. Use plain English and include practical examples."
)
return {
"item": item,
"item_type": item_type,
"explanation": result.get("message", ""),
"success": result.get("success", False)
}
def _detect_item_type(item: str) -> str:
"""Detect what type of item is being explained."""
item_lower = item.lower()
# Check for command patterns
if any(tool in item_lower for tool in ['nmap', 'nikto', 'gobuster', 'sqlmap', 'hydra']):
return "command"
# Check for error patterns
if any(word in item_lower for word in ['error', 'exception', 'failed', 'denied']):
return "error"
# Check for config patterns
if '=' in item or ':' in item or 'config' in item_lower:
return "config"
# Check for scan result patterns
if any(word in item_lower for word in ['open', 'closed', 'filtered', 'vulnerability', 'port']):
return "scan_result"
# Default to concept
return "concept"
async def suggest_config(
config_type: str,
current_values: Optional[Dict] = None,
use_case: Optional[str] = None
) -> Dict[str, Any]:
"""
Get LLM-powered configuration suggestions.
Args:
config_type: Type of configuration (scan, system, security)
current_values: Current configuration values
use_case: Specific use case or scenario
Returns:
Dictionary with configuration suggestions
"""
prompt_parts = [f"Suggest optimal configuration for {config_type}."]
if current_values:
prompt_parts.append(f"\nCurrent configuration:\n{json.dumps(current_values, indent=2)}")
if use_case:
prompt_parts.append(f"\nUse case: {use_case}")
prompt_parts.append("\nProvide recommended values with explanations. Format as JSON if possible.")
result = await chat_completion(
message="\n".join(prompt_parts),
system_prompt="You are a security configuration expert. Provide optimal, secure, and practical configuration recommendations."
)
# Try to extract JSON from response
response_text = result.get("message", "")
suggested_config = _extract_json_from_text(response_text)
return {
"config_type": config_type,
"suggestions": suggested_config or {},
"explanation": response_text,
"success": result.get("success", False)
}
def _extract_json_from_text(text: str) -> Optional[Dict]:
"""Try to extract JSON object from text."""
try:
# Look for JSON object in text
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1:
json_str = text[start:end+1]
return json.loads(json_str)
except json.JSONDecodeError:
pass
return None
async def get_step_by_step(
task: str,
skill_level: str = "intermediate"
) -> Dict[str, Any]:
"""
Get step-by-step instructions for a task.
Args:
task: The task to get instructions for
skill_level: User skill level (beginner, intermediate, advanced)
Returns:
Dictionary with step-by-step instructions
"""
skill_context = {
"beginner": "Explain in simple terms, avoid jargon, include screenshots references",
"intermediate": "Provide clear steps with command examples",
"advanced": "Be concise, focus on efficiency and best practices"
}
context = skill_context.get(skill_level, skill_context["intermediate"])
prompt = f"""Provide step-by-step instructions for: {task}
User skill level: {skill_level}
{context}
Format as numbered steps with clear actions. Include any commands to run."""
result = await chat_completion(
message=prompt,
system_prompt="You are an expert security instructor. Provide clear, actionable step-by-step guidance."
)
# Parse steps from response
steps = _parse_steps_from_text(result.get("message", ""))
return {
"task": task,
"skill_level": skill_level,
"steps": steps,
"full_explanation": result.get("message", ""),
"success": result.get("success", False)
}
def _parse_steps_from_text(text: str) -> List[Dict[str, str]]:
"""Parse numbered steps from text."""
steps = []
lines = text.split('\n')
for line in lines:
# Match patterns like "1.", "Step 1:", "1)"
import re
match = re.match(r'^(?:Step\s+)?(\d+)[.):]\s*(.+)$', line.strip(), re.IGNORECASE)
if match:
step_num = int(match.group(1))
step_text = match.group(2).strip()
steps.append({
"number": step_num,
"instruction": step_text
})
return steps
def clear_conversation_context(session_id: str) -> bool:
"""
Clear conversation context for a session.
Args:
session_id: Session ID to clear
Returns:
True if cleared, False if session didn't exist
"""
if session_id in conversation_contexts:
del conversation_contexts[session_id]
return True
return False
def get_conversation_summary(session_id: str) -> Dict[str, Any]:
"""
Get summary of conversation for a session.
Args:
session_id: Session ID
Returns:
Dictionary with conversation summary
"""
if session_id not in conversation_contexts:
return {
"session_id": session_id,
"exists": False,
"message_count": 0
}
messages = conversation_contexts[session_id]
user_messages = [m for m in messages if m["role"] == "user"]
return {
"session_id": session_id,
"exists": True,
"message_count": len(messages),
"user_message_count": len(user_messages),
"last_messages": messages[-5:] if messages else []
}

View File

@@ -0,0 +1,505 @@
"""
Nmap Parser Module
Parses Nmap XML or JSON output to extract host information including:
- IP addresses, hostnames
- Operating system detection
- Device type classification (workstation/server/appliance)
- MAC vendor information
- Open ports and services
"""
import xml.etree.ElementTree as ET
import json
from typing import Dict, List, Any, Optional
import re
def parse_nmap_xml(xml_content: str) -> List[Dict[str, Any]]:
"""
Parse Nmap XML output and extract host information.
Args:
xml_content: Raw XML string from nmap -oX output
Returns:
List of host dictionaries with parsed information
"""
hosts = []
try:
# Clean up XML content - remove any non-XML content before the declaration
xml_start = xml_content.find('<?xml')
if xml_start == -1:
xml_start = xml_content.find('<nmaprun')
if xml_start > 0:
xml_content = xml_content[xml_start:]
root = ET.fromstring(xml_content)
for host_elem in root.findall('.//host'):
# Check if host is up
status = host_elem.find('status')
if status is None or status.get('state') != 'up':
continue
host = _parse_host_element(host_elem)
if host.get('ip'):
hosts.append(host)
except ET.ParseError as e:
print(f"XML parsing error: {e}")
# Return empty list on parse error
return []
return hosts
def parse_nmap_json(json_content: str) -> List[Dict[str, Any]]:
"""
Parse Nmap JSON output and extract host information.
Args:
json_content: JSON string from nmap with JSON output
Returns:
List of host dictionaries with parsed information
"""
hosts = []
try:
data = json.loads(json_content)
# Handle different JSON structures
if isinstance(data, list):
scan_results = data
elif isinstance(data, dict):
# Try common JSON nmap output structures
scan_results = data.get('hosts', data.get('scan', []))
else:
return []
for host_data in scan_results:
host = _parse_host_json(host_data)
if host.get('ip'):
hosts.append(host)
except json.JSONDecodeError as e:
print(f"JSON parsing error: {e}")
return []
return hosts
def _parse_host_element(host_elem: ET.Element) -> Dict[str, Any]:
"""
Parse an individual host XML element.
Args:
host_elem: XML Element representing a single host
Returns:
Dictionary with host information
"""
host = {
'ip': '',
'hostname': '',
'mac': '',
'vendor': '',
'os_type': '',
'os_details': '',
'device_type': '',
'ports': [],
'os_accuracy': 0
}
# Extract IP address
addr = host_elem.find("address[@addrtype='ipv4']")
if addr is not None:
host['ip'] = addr.get('addr', '')
# Extract MAC address and vendor
mac = host_elem.find("address[@addrtype='mac']")
if mac is not None:
host['mac'] = mac.get('addr', '')
host['vendor'] = mac.get('vendor', '')
# Extract hostname
hostname_elem = host_elem.find(".//hostname")
if hostname_elem is not None:
host['hostname'] = hostname_elem.get('name', '')
# Extract OS information
osmatch = host_elem.find(".//osmatch")
if osmatch is not None:
os_name = osmatch.get('name', '')
host['os_details'] = os_name
host['os_type'] = detect_os_type(os_name)
try:
host['os_accuracy'] = int(osmatch.get('accuracy', 0))
except (ValueError, TypeError):
host['os_accuracy'] = 0
else:
# Try osclass as fallback
osclass = host_elem.find(".//osclass")
if osclass is not None:
osfamily = osclass.get('osfamily', '')
osgen = osclass.get('osgen', '')
host['os_type'] = detect_os_type(osfamily)
host['os_details'] = f"{osfamily} {osgen}".strip()
try:
host['os_accuracy'] = int(osclass.get('accuracy', 0))
except (ValueError, TypeError):
host['os_accuracy'] = 0
# Extract ports
for port_elem in host_elem.findall(".//port"):
port_info = {
'port': int(port_elem.get('portid', 0)),
'protocol': port_elem.get('protocol', 'tcp'),
'state': '',
'service': '',
'product': '',
'version': ''
}
state_elem = port_elem.find('state')
if state_elem is not None:
port_info['state'] = state_elem.get('state', '')
service_elem = port_elem.find('service')
if service_elem is not None:
port_info['service'] = service_elem.get('name', '')
port_info['product'] = service_elem.get('product', '')
port_info['version'] = service_elem.get('version', '')
# Use service info to help detect OS if not already detected
if not host['os_type']:
product = service_elem.get('product', '').lower()
if 'microsoft' in product or 'windows' in product:
host['os_type'] = 'Windows'
elif 'apache' in product or 'nginx' in product or 'linux' in product:
host['os_type'] = 'Linux'
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'])
# Classify device type
host['device_type'] = classify_device_type(host)
return host
def _parse_host_json(host_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Parse host data from JSON format.
Args:
host_data: Dictionary containing host information
Returns:
Standardized host dictionary
"""
host = {
'ip': host_data.get('ip', host_data.get('address', '')),
'hostname': host_data.get('hostname', host_data.get('name', '')),
'mac': host_data.get('mac', ''),
'vendor': host_data.get('vendor', ''),
'os_type': '',
'os_details': '',
'device_type': '',
'ports': [],
'os_accuracy': 0
}
# Extract OS information
os_info = host_data.get('os', host_data.get('osmatch', {}))
if isinstance(os_info, dict):
host['os_details'] = os_info.get('name', os_info.get('details', ''))
host['os_accuracy'] = int(os_info.get('accuracy', 0))
elif isinstance(os_info, str):
host['os_details'] = os_info
host['os_type'] = detect_os_type(host['os_details'])
# Extract ports
ports_data = host_data.get('ports', host_data.get('tcp', {}))
if isinstance(ports_data, list):
host['ports'] = ports_data
elif isinstance(ports_data, dict):
for port_num, port_info in ports_data.items():
if isinstance(port_info, dict):
host['ports'].append({
'port': int(port_num),
'protocol': 'tcp',
'state': port_info.get('state', ''),
'service': port_info.get('service', port_info.get('name', '')),
'product': port_info.get('product', ''),
'version': port_info.get('version', '')
})
# Infer OS from ports if unknown
if not host['os_type'] and host['ports']:
host['os_type'] = _infer_os_from_ports(host['ports'])
# Classify device type
host['device_type'] = classify_device_type(host)
return host
def detect_os_type(os_string: str) -> str:
"""
Detect OS type from an OS description string.
Args:
os_string: OS description from nmap
Returns:
Standardized OS type string
"""
if not os_string:
return 'Unknown'
os_lower = os_string.lower()
# Windows detection
if any(keyword in os_lower for keyword in ['windows', 'microsoft', 'win7', 'win10', 'win11', 'server 20']):
return 'Windows'
# Linux detection
elif any(keyword in os_lower for keyword in ['linux', 'ubuntu', 'debian', 'centos', 'red hat', 'rhel', 'fedora', 'arch', 'gentoo', 'suse']):
return 'Linux'
# macOS detection
elif any(keyword in os_lower for keyword in ['mac os', 'darwin', 'apple', 'macos']):
return 'macOS'
# Unix variants
elif any(keyword in os_lower for keyword in ['freebsd', 'openbsd', 'netbsd', 'unix', 'solaris', 'aix']):
return 'Unix'
# Network devices
elif any(keyword in os_lower for keyword in ['cisco', 'ios']):
return 'Cisco'
elif 'juniper' in os_lower or 'junos' in os_lower:
return 'Juniper'
elif 'fortinet' in os_lower or 'fortigate' in os_lower:
return 'Fortinet'
elif 'palo alto' in os_lower or 'panos' in os_lower:
return 'Palo Alto'
elif any(keyword in os_lower for keyword in ['switch', 'router', 'firewall', 'gateway']):
return 'Network Device'
# Virtualization
elif 'vmware' in os_lower or 'esxi' in os_lower:
return 'VMware'
elif 'hyper-v' in os_lower:
return 'Hyper-V'
# Mobile
elif 'android' in os_lower:
return 'Android'
elif 'ios' in os_lower and 'apple' in os_lower:
return 'iOS'
# Printers and IoT
elif any(keyword in os_lower for keyword in ['printer', 'hp jetdirect', 'canon', 'epson', 'xerox']):
return 'Printer'
elif 'iot' in os_lower or 'embedded' in os_lower:
return 'IoT Device'
return 'Unknown'
def classify_device_type(host: Dict[str, Any]) -> str:
"""
Classify the device type based on OS, ports, and services.
Args:
host: Host dictionary with OS and port information
Returns:
Device type classification (workstation, server, network, appliance, etc.)
"""
os_type = host.get('os_type', '').lower()
os_details = host.get('os_details', '').lower()
ports = host.get('ports', [])
vendor = host.get('vendor', '').lower()
port_numbers = {p['port'] for p in ports}
services = {p.get('service', '').lower() for p in ports}
# Network infrastructure
if os_type in ['cisco', 'juniper', 'fortinet', 'palo alto', 'network device']:
if 'switch' in os_details or 'catalyst' in os_details:
return 'Network Switch'
elif 'router' in os_details or 'ios' in os_details:
return 'Router'
elif 'firewall' in os_details or 'fortigate' in os_details:
return 'Firewall'
else:
return 'Network Device'
# Check for SNMP (common on network devices)
if 161 in port_numbers or 162 in port_numbers:
return 'Network Device'
# Printers
if os_type == 'printer' or 9100 in port_numbers or 631 in port_numbers:
return 'Printer'
# IoT devices
if os_type == 'iot device':
return 'IoT Device'
# Servers - check for common server ports and services
server_indicators = {
# Web servers
80, 443, 8080, 8443,
# Database servers
3306, 5432, 1433, 27017, 6379,
# Mail servers
25, 587, 465, 110, 995, 143, 993,
# File servers
21, 22, 139, 445, 2049,
# Directory services
389, 636, 88, 464,
# Application servers
8000, 8001, 8888, 9000, 3000, 5000,
# Virtualization
902, 443
}
server_services = {
'http', 'https', 'apache', 'nginx', 'iis',
'mysql', 'postgresql', 'mssql', 'mongodb', 'redis',
'smtp', 'pop3', 'imap',
'ftp', 'ssh', 'smb', 'nfs',
'ldap', 'ldaps', 'kerberos',
'vmware'
}
# Check if it's explicitly a server OS
if 'server' in os_details:
return 'Server'
# Check for server ports/services
if port_numbers & server_indicators or services & server_services:
# More than 3 server ports suggests a server
if len(port_numbers & server_indicators) >= 3:
return 'Server'
# Specific database or web server services
if any(svc in services for svc in ['mysql', 'postgresql', 'mongodb', 'apache', 'nginx', 'iis']):
return 'Server'
# Virtualization hosts
if os_type in ['vmware', 'hyper-v'] or 'esxi' in os_details:
return 'Virtualization Host'
# Workstations
if os_type in ['windows', 'macos', 'linux']:
# Windows/macOS are typically workstations unless server indicators
if os_type in ['windows', 'macos']:
if 3389 in port_numbers: # RDP
# Could be either, but default to workstation
return 'Workstation'
return 'Workstation'
# Linux could be either
elif os_type == 'linux':
# Desktop Linux if few ports open
if len(port_numbers) <= 3:
return 'Workstation'
else:
return 'Server'
# Mobile devices
if os_type in ['android', 'ios']:
return 'Mobile Device'
# Default classification
if len(port_numbers) >= 5:
return 'Server'
elif len(port_numbers) >= 1:
return 'Workstation'
return 'Unknown'
def _infer_os_from_ports(ports: List[Dict[str, Any]]) -> str:
"""
Infer OS type from open ports and services.
Args:
ports: List of port dictionaries
Returns:
Inferred OS type
"""
port_numbers = {p['port'] for p in ports}
services = [p.get('service', '').lower() for p in ports]
products = [p.get('product', '').lower() for p in ports]
# Windows indicators
windows_ports = {135, 139, 445, 3389, 5985, 5986}
if windows_ports & port_numbers:
return 'Windows'
if any('microsoft' in p or 'windows' in p for p in products):
return 'Windows'
# Linux indicators (SSH is common)
if 22 in port_numbers and 'ssh' in services:
# Could be Linux or Unix
return 'Linux'
# Network device indicators
if 161 in port_numbers or 162 in port_numbers: # SNMP
return 'Network Device'
if 23 in port_numbers: # Telnet (often network devices)
return 'Network Device'
# Printer indicators
if 9100 in port_numbers or 631 in port_numbers:
return 'Printer'
return 'Unknown'
def get_os_icon_name(host: Dict[str, Any]) -> str:
"""
Get the appropriate icon name for a host based on OS and device type.
Args:
host: Host dictionary
Returns:
Icon filename (without extension)
"""
os_type = host.get('os_type', '').lower()
device_type = host.get('device_type', '').lower()
# Device type takes precedence for specialized devices
if 'server' in device_type:
return 'server'
elif 'network' in device_type or 'router' in device_type or 'switch' in device_type or 'firewall' in device_type:
return 'network'
elif 'printer' in device_type:
return 'printer'
elif 'workstation' in device_type:
return 'workstation'
# Fall back to OS type
if 'windows' in os_type:
return 'windows'
elif 'linux' in os_type or 'unix' in os_type:
return 'linux'
elif 'mac' in os_type:
return 'mac'
elif any(net in os_type for net in ['cisco', 'juniper', 'fortinet', 'network']):
return 'network'
return 'unknown'

View File

@@ -0,0 +1,508 @@
"""
Voice Control Module
Handles speech-to-text and text-to-speech functionality, plus voice command routing.
Supports local Whisper (preferred) and OpenAI API as fallback.
"""
import os
import tempfile
from typing import Dict, Any, Optional, Tuple
import json
import re
def transcribe_audio(audio_data: bytes, format: str = "wav") -> Dict[str, Any]:
"""
Transcribe audio to text using Whisper (local preferred) or OpenAI API.
Args:
audio_data: Raw audio bytes
format: Audio format (wav, mp3, webm, etc.)
Returns:
Dictionary with transcription result and metadata
{
"text": "transcribed text",
"language": "en",
"confidence": 0.95,
"method": "whisper-local" or "openai"
}
"""
# Try local Whisper first
try:
return _transcribe_with_local_whisper(audio_data, format)
except Exception as e:
print(f"Local Whisper failed: {e}, falling back to OpenAI API")
# Fallback to OpenAI API if configured
if os.getenv("OPENAI_API_KEY"):
try:
return _transcribe_with_openai(audio_data, format)
except Exception as e:
print(f"OpenAI transcription failed: {e}")
return {
"text": "",
"error": f"Transcription failed: {str(e)}",
"method": "none"
}
return {
"text": "",
"error": "No transcription service available. Install Whisper or configure OPENAI_API_KEY.",
"method": "none"
}
def _transcribe_with_local_whisper(audio_data: bytes, format: str) -> Dict[str, Any]:
"""
Transcribe using local Whisper model.
Args:
audio_data: Raw audio bytes
format: Audio format
Returns:
Transcription result dictionary
"""
try:
import whisper
# Save audio to temporary file
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as temp_audio:
temp_audio.write(audio_data)
temp_audio_path = temp_audio.name
try:
# Load model (use base model by default for speed/accuracy balance)
model_size = os.getenv("WHISPER_MODEL", "base")
model = whisper.load_model(model_size)
# Transcribe
result = model.transcribe(temp_audio_path)
return {
"text": result["text"].strip(),
"language": result.get("language", "unknown"),
"confidence": 1.0, # Whisper doesn't provide confidence scores
"method": "whisper-local",
"model": model_size
}
finally:
# Clean up temp file
try:
os.unlink(temp_audio_path)
except:
pass
except ImportError:
raise Exception("Whisper not installed. Install with: pip install openai-whisper")
def _transcribe_with_openai(audio_data: bytes, format: str) -> Dict[str, Any]:
"""
Transcribe using OpenAI Whisper API.
Args:
audio_data: Raw audio bytes
format: Audio format
Returns:
Transcription result dictionary
"""
try:
import httpx
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise Exception("OPENAI_API_KEY not configured")
# Prepare multipart form data
files = {
'file': (f'audio.{format}', audio_data, f'audio/{format}')
}
data = {
'model': 'whisper-1',
'language': 'en' # Can be auto-detected by omitting this
}
# Make API request
with httpx.Client() as client:
response = client.post(
'https://api.openai.com/v1/audio/transcriptions',
headers={'Authorization': f'Bearer {api_key}'},
files=files,
data=data,
timeout=30.0
)
if response.status_code == 200:
result = response.json()
return {
"text": result.get("text", "").strip(),
"language": "en",
"confidence": 1.0,
"method": "openai"
}
else:
raise Exception(f"OpenAI API error: {response.status_code} - {response.text}")
except ImportError:
raise Exception("httpx not installed")
def speak_text(text: str, voice: str = "alloy", format: str = "mp3") -> Optional[bytes]:
"""
Convert text to speech using OpenAI TTS, Coqui, or browser fallback.
Args:
text: Text to convert to speech
voice: Voice selection (depends on TTS engine)
format: Audio format (mp3, wav, opus)
Returns:
Audio bytes or None if TTS not available
"""
# Try OpenAI TTS if configured
if os.getenv("OPENAI_API_KEY"):
try:
return _tts_with_openai(text, voice, format)
except Exception as e:
print(f"OpenAI TTS failed: {e}")
# Try local Coqui TTS
try:
return _tts_with_coqui(text)
except Exception as e:
print(f"Coqui TTS failed: {e}")
# Return None to signal browser should handle TTS
return None
def _tts_with_openai(text: str, voice: str, format: str) -> bytes:
"""
Text-to-speech using OpenAI TTS API.
Args:
text: Text to speak
voice: Voice name (alloy, echo, fable, onyx, nova, shimmer)
format: Audio format
Returns:
Audio bytes
"""
try:
import httpx
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise Exception("OPENAI_API_KEY not configured")
# Valid voices for OpenAI TTS
valid_voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
if voice not in valid_voices:
voice = "alloy"
# Valid formats
valid_formats = ["mp3", "opus", "aac", "flac"]
if format not in valid_formats:
format = "mp3"
with httpx.Client() as client:
response = client.post(
'https://api.openai.com/v1/audio/speech',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
json={
'model': 'tts-1', # or 'tts-1-hd' for higher quality
'input': text[:4096], # Max 4096 characters
'voice': voice,
'response_format': format
},
timeout=30.0
)
if response.status_code == 200:
return response.content
else:
raise Exception(f"OpenAI TTS error: {response.status_code} - {response.text}")
except ImportError:
raise Exception("httpx not installed")
def _tts_with_coqui(text: str) -> bytes:
"""
Text-to-speech using Coqui TTS (local).
Args:
text: Text to speak
Returns:
Audio bytes (WAV format)
"""
try:
from TTS.api import TTS
import numpy as np
import io
import wave
# Initialize TTS with a fast model
tts = TTS(model_name="tts_models/en/ljspeech/tacotron2-DDC", progress_bar=False)
# Generate speech
wav = tts.tts(text)
# Convert to WAV bytes
wav_io = io.BytesIO()
with wave.open(wav_io, 'wb') as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(22050)
wav_file.writeframes(np.array(wav * 32767, dtype=np.int16).tobytes())
return wav_io.getvalue()
except ImportError:
raise Exception("Coqui TTS not installed. Install with: pip install TTS")
def parse_voice_command(text: str) -> Dict[str, Any]:
"""
Parse voice command text to extract intent and parameters.
Args:
text: Transcribed voice command text
Returns:
Dictionary with command intent and parameters
{
"intent": "list_agents" | "summarize" | "deploy_agent" | "run_scan" | "unknown",
"parameters": {...},
"confidence": 0.0-1.0
}
"""
text_lower = text.lower().strip()
# Command patterns
patterns = [
# List commands
(r'\b(list|show|display)\s+(agents|scans|findings|results)\b', 'list', lambda m: {'target': m.group(2)}),
# Summarize commands
(r'\b(summarize|summary of|sum up)\s+(findings|results|scan)\b', 'summarize', lambda m: {'target': m.group(2)}),
# Deploy/start commands
(r'\b(deploy|start|launch|run)\s+agent\s+(?:on\s+)?(.+)', 'deploy_agent', lambda m: {'target': m.group(2).strip()}),
# Scan commands
(r'\b(scan|nmap|enumerate)\s+(.+?)(?:\s+(?:using|with)\s+(\w+))?$', 'run_scan',
lambda m: {'target': m.group(2).strip(), 'tool': m.group(3) if m.group(3) else 'nmap'}),
# Status commands
(r'\b(status|what\'?s\s+(?:the\s+)?status)\b', 'get_status', lambda m: {}),
# Help commands
(r'\b(help|how\s+do\s+i|assist)\b', 'help', lambda m: {'query': text}),
# Clear/stop commands
(r'\b(stop|cancel|clear)\s+(scan|all|everything)\b', 'stop', lambda m: {'target': m.group(2)}),
# Navigate commands
(r'\b(go\s+to|open|navigate\s+to)\s+(.+)', 'navigate', lambda m: {'destination': m.group(2).strip()}),
]
# Try to match patterns
for pattern, intent, param_func in patterns:
match = re.search(pattern, text_lower)
if match:
try:
parameters = param_func(match)
return {
"intent": intent,
"parameters": parameters,
"confidence": 0.85,
"raw_text": text
}
except Exception as e:
print(f"Error parsing command parameters: {e}")
# No pattern matched
return {
"intent": "unknown",
"parameters": {},
"confidence": 0.0,
"raw_text": text
}
def route_command(command_result: Dict[str, Any]) -> Dict[str, Any]:
"""
Route a parsed voice command to the appropriate action.
Args:
command_result: Result from parse_voice_command()
Returns:
Dictionary with routing information
{
"action": "api_call" | "navigate" | "notify" | "error",
"endpoint": "/api/...",
"method": "GET" | "POST",
"data": {...},
"message": "Human-readable action description"
}
"""
intent = command_result.get("intent")
params = command_result.get("parameters", {})
if intent == "list":
target = params.get("target", "")
endpoint_map = {
"agents": "/api/agents",
"scans": "/api/scans",
"findings": "/api/findings",
"results": "/api/results"
}
endpoint = endpoint_map.get(target, "/api/scans")
return {
"action": "api_call",
"endpoint": endpoint,
"method": "GET",
"data": {},
"message": f"Fetching {target}..."
}
elif intent == "summarize":
target = params.get("target", "findings")
return {
"action": "api_call",
"endpoint": "/api/summarize",
"method": "POST",
"data": {"target": target},
"message": f"Summarizing {target}..."
}
elif intent == "deploy_agent":
target = params.get("target", "")
return {
"action": "api_call",
"endpoint": "/api/agents/deploy",
"method": "POST",
"data": {"target": target},
"message": f"Deploying agent to {target}..."
}
elif intent == "run_scan":
target = params.get("target", "")
tool = params.get("tool", "nmap")
return {
"action": "api_call",
"endpoint": "/api/scan",
"method": "POST",
"data": {
"tool": tool,
"target": target,
"scan_type": "quick"
},
"message": f"Starting {tool} scan of {target}..."
}
elif intent == "get_status":
return {
"action": "api_call",
"endpoint": "/api/status",
"method": "GET",
"data": {},
"message": "Checking system status..."
}
elif intent == "help":
query = params.get("query", "")
return {
"action": "api_call",
"endpoint": "/api/llm/chat",
"method": "POST",
"data": {"message": query, "context": "help_request"},
"message": "Getting help..."
}
elif intent == "stop":
target = params.get("target", "all")
return {
"action": "api_call",
"endpoint": "/api/scans/clear" if target in ["all", "everything"] else "/api/scan/stop",
"method": "DELETE",
"data": {},
"message": f"Stopping {target}..."
}
elif intent == "navigate":
destination = params.get("destination", "")
# Map common destinations
destination_map = {
"dashboard": "/",
"home": "/",
"terminal": "/terminal",
"scans": "/scans",
"settings": "/settings"
}
path = destination_map.get(destination, f"/{destination}")
return {
"action": "navigate",
"endpoint": path,
"method": "GET",
"data": {},
"message": f"Navigating to {destination}..."
}
else:
# Unknown intent - return error
return {
"action": "error",
"endpoint": "",
"method": "",
"data": {},
"message": "I didn't understand that command. Try 'help' for available commands.",
"error": "unknown_intent"
}
def get_voice_command_help() -> Dict[str, list]:
"""
Get list of available voice commands.
Returns:
Dictionary categorized by command type
"""
return {
"navigation": [
"Go to dashboard",
"Open terminal",
"Navigate to scans"
],
"scanning": [
"Scan 192.168.1.1",
"Run nmap scan on example.com",
"Start scan of 10.0.0.0/24"
],
"information": [
"List scans",
"Show agents",
"Display findings",
"What's the status"
],
"actions": [
"Deploy agent on target.com",
"Stop all scans",
"Clear everything",
"Summarize findings"
],
"help": [
"Help me with nmap",
"How do I scan a network",
"Assist with reconnaissance"
]
}

View File

@@ -2,3 +2,4 @@ fastapi==0.115.5
uvicorn[standard]==0.32.1
httpx==0.28.1
pydantic==2.10.2
python-multipart==0.0.9