Add backend modules and frontend components for StrikePackageGPT expansion
Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com>
345
services/dashboard/ExplainButton.jsx
Normal 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;
|
||||
487
services/dashboard/GuidedWizard.jsx
Normal 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;
|
||||
424
services/dashboard/HelpChat.jsx
Normal 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;
|
||||
315
services/dashboard/NetworkMap.jsx
Normal 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;
|
||||
354
services/dashboard/VoiceControls.jsx
Normal 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;
|
||||
9
services/dashboard/static/linux.svg
Normal 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 |
8
services/dashboard/static/mac.svg
Normal 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 |
16
services/dashboard/static/network.svg
Normal 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 |
15
services/dashboard/static/server.svg
Normal 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 |
5
services/dashboard/static/unknown.svg
Normal 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 |
7
services/dashboard/static/windows.svg
Normal 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 |
9
services/dashboard/static/workstation.svg
Normal 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 |
516
services/hackgpt-api/app/config_validator.py
Normal 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))
|
||||
547
services/hackgpt-api/app/explain.py
Normal 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
|
||||
435
services/hackgpt-api/app/llm_help.py
Normal 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 []
|
||||
}
|
||||
505
services/hackgpt-api/app/nmap_parser.py
Normal 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'
|
||||
508
services/hackgpt-api/app/voice.py
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||